refactor(host/W6.2): extract the input-injection backends into the pf-inject crate

inject.rs + inject/* (the per-OS injectors — wlroots virtual-input, KWin
fake_input, libei/reis, gamescope-EI on Linux; SendInput on Windows — plus the
virtual-gamepad HID stack: DualSense/DualShock4/Switch Pro/Steam Controller/Deck
over uhid/usbip and the Windows UMDF drivers, the proto codecs, the injector
service, and the uhid manager) move into crates/pf-inject behind the
InputInjector trait (plan §W6). It consumes punktfunk_core::input (the neutral
GamepadEvent/InputEvent vocabulary, moved to core in W5) + the pf-driver-proto
wire contract, and reaches pf-capture only for the Windows gamepad-channel
WUDFHost check + the resident-mouse compose-kick hook.

The one inject->vdisplay coupling (the libei gamescope-EI backend needs the EIS
relay socket path) is broken via a leaf: gamescope_ei_socket_file moves to
pf-paths as the shared contract — the gamescope producer (host vdisplay) keeps
its session-env-lock wrapper around it, the libei consumer (pf-inject) reads it
directly post-retarget. The host keeps a `mod inject { pub use pf_inject::* }`
shim so every crate::inject::* path (the native/gamestream input planes + devtest)
is unchanged; the heavy input deps (wayland/reis/xkbcommon/usbip + the KWin
fake-input protocol XML) moved with the crate.

Verified: Linux clippy -D warnings (pf-inject + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-inject 69/69 + host 230/230 tests; Windows clippy -D warnings
(pf-inject --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 11:52:02 +02:00
co-authored by Claude Opus 4.8
parent 0992548de7
commit f6c6e4e594
43 changed files with 199 additions and 73 deletions
@@ -0,0 +1,90 @@
//! Virtual Sony DualSense **Edge** on Windows via the UMDF minidriver — the Edge sibling of
//! [`super::dualsense_windows`]. Same transport ([`DsWinPad`]: a per-session `SwDeviceCreate`
//! devnode + the sealed shared-memory channel), same report codec ([`super::dualsense_proto`]);
//! the host stamps `device_type = 2` so the one UMDF driver serves the Edge descriptor /
//! `VID_054C&PID_0DF2` attributes, and the wire back-grip bits map onto the Edge's native
//! `buttons[2]` slots instead of the fold/drop policy — a client's Deck grips / Elite paddles
//! reach games as real buttons. Feedback is identical to the plain DualSense (rumble arrives with
//! the vibration-v2 flag, which [`parse_ds_output`](super::dualsense_proto::parse_ds_output)
//! already handles).
use super::dualsense_proto::{edge_paddle_bits, DsState, DS_TOUCH_H, DS_TOUCH_W};
use super::dualsense_windows::{DsWinPad, WinDsIdentity};
use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::Result;
use punktfunk_core::quic::RichInput;
/// The Windows-Edge half of the shared stateful manager (see [`PadProto`]): the shared
/// [`DsWinPad`] transport under the Edge identity, with the Edge paddle mapping in `merge_frame`.
/// No remap config — every wire paddle has a native slot.
#[derive(Default)]
pub struct DsEdgeWinProto;
impl PadProto for DsEdgeWinProto {
type Pad = DsWinPad;
type State = DsState;
const LABEL: &'static str = "DualSense Edge/Windows";
const DEVICE: &'static str = "DualSense Edge";
const CREATE_HINT: &'static str =
" (install/repair: punktfunk-host.exe driver install --gamepad)";
fn open(&mut self, idx: u8) -> Result<DsWinPad> {
let p = DsWinPad::open(idx, &WinDsIdentity::dualsense_edge())?;
tracing::info!(
index = idx,
"virtual DualSense Edge created (Windows UMDF shm channel)"
);
Ok(p)
}
fn neutral(&self) -> DsState {
DsState::neutral()
}
/// Merge buttons/sticks/triggers from the frame, preserving the rich-plane fields — like the
/// plain DualSense, EXCEPT the wire paddles land on the Edge's own `buttons[2]` bits
/// (rebuilt from every button frame, so no extra persistence).
fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState {
let mut s = DsState::from_gamepad(
f.buttons,
f.ls_x,
f.ls_y,
f.rs_x,
f.rs_y,
f.left_trigger,
f.right_trigger,
);
s.buttons[2] |= edge_paddle_bits(f.buttons);
s.touch = prev.touch;
s.gyro = prev.gyro;
s.accel = prev.accel;
s.touch_click = prev.touch_click;
s
}
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
/// split the one touchpad left/right, pad clicks ride touch_click.
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
st.apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
}
fn write_state(&self, pad: &mut DsWinPad, st: &DsState) {
pad.write_state(st);
}
/// Poll the section for a game's feedback: motor rumble on the universal 0xCA plane, the rich
/// lightbar/player-LED/trigger events on the 0xCD plane.
fn service(&self, pad: &mut DsWinPad, idx: u8) -> PadFeedback {
let fb = pad.service(idx);
PadFeedback {
rumble: fb.rumble,
hidout: fb.hidout,
game_drove: Some(fb.fresh),
}
}
}
/// All virtual DualSense Edge pads of a session — the Windows analogue of
/// [`DualSenseEdgeManager`](crate::dualsense::DualSenseEdgeManager), with the same method
/// surface (via the shared [`UhidManager`]) as the other Windows pad managers.
pub type DualSenseEdgeWindowsManager = UhidManager<DsEdgeWinProto>;
@@ -0,0 +1,563 @@
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/drivers/pf-dualsense`).
//!
//! The Windows analogue of the Linux UHID backend ([`super::dualsense`]): same [`DsState`] model and
//! the same byte-level report codec ([`super::dualsense_proto`]), but a different transport. Where
//! the Linux backend writes report `0x01` to `/dev/uhid` and reads report `0x02` via `UHID_OUTPUT`,
//! the Windows backend talks to the UMDF driver over an **unnamed shared DATA section** (256 B `PadShm`:
//! magic `u32@0`, input report `@8`, output seq `u32@72`, output report `@76`) reached over the
//! **sealed channel** ([`PadChannel`], `design/gamepad-channel-sealing.md`): the host duplicates the
//! section handle into the driver's WUDFHost, bootstrapped via the named `Global\pfds-boot-<idx>`
//! mailbox. The driver feeds game `READ_REPORT`s from the input bytes and publishes a game's `0x02`
//! (rumble / lightbar / player-LEDs / adaptive triggers) into the output 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`.
//!
//! Device lifecycle: each pad `SwDeviceCreate`s a `pf_pad_<index>` software devnode (hardware id
//! `pf_dualsense`, enumerator `punktfunk`) on open and `SwDeviceClose`s it on drop, so the virtual
//! 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::{
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
DS_TOUCH_W,
};
use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx};
use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::{anyhow, Result};
use punktfunk_core::quic::RichInput;
use std::ffi::c_void;
use std::sync::atomic::{fence, AtomicU32, Ordering};
use std::time::Duration;
use windows::core::{w, GUID, PCWSTR};
use windows::Win32::Devices::Enumeration::Pnp::{
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
};
use windows::Win32::Foundation::{CloseHandle, E_FAIL, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject};
/// Shared-section layout — the single source of truth is [`pf_driver_proto::gamepad::PadShm`] (offset
/// asserts pin every field; the `pf_dualsense` driver maps the same struct). Derive the size/offsets/magic
/// from it so a layout change is a compile error, not a hand-synced literal (audit §6.1). `pub(super)` so
/// the sibling DualShock 4 backend ([`super::dualshock4_windows`]) reuses the exact offsets.
pub(super) const SHM_SIZE: usize = core::mem::size_of::<pf_driver_proto::gamepad::PadShm>();
pub(super) const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS"
pub(super) const OFF_INPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, input);
pub(super) const OFF_OUT_SEQ: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, out_seq);
pub(super) const OFF_OUTPUT: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, output);
/// Device-type selector the driver reads to choose which HID identity/descriptor it serves: 0 =
/// DualSense (the default — the section is zeroed), 1 = DualShock 4.
pub(super) const OFF_DEVTYPE: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, device_type);
pub(super) const OFF_DRIVER_PROTO: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, driver_proto);
pub(super) const OFF_PAD_INDEX: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, pad_index);
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSHOCK4;
pub(super) const DEVTYPE_DUALSENSE_EDGE: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSENSE_EDGE;
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait), like the
/// Linux pads.
pub struct DsWinPad {
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
_sw: Option<super::gamepad_raii::SwDevice>,
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
channel: PadChannel,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
seq: u8,
ts: u32,
last_out_seq: u32,
}
/// The PnP identity for a virtual controller devnode — varies by controller type so the same
/// [`create_swdevice`] builds a DualSense (`VID_054C&PID_0CE6`) or a DualShock 4
/// (`VID_054C&PID_09CC`). The fields map onto the `SW_DEVICE_CREATE_INFO` identity discussed below.
pub(super) struct SwDeviceProfile<'a> {
/// PnP instance id — distinct namespaces per type (`pf_pad_<idx>` vs `pf_ds4_<idx>`) so the two
/// never reuse the same devnode shell.
pub instance: &'a str,
/// `Data1` of the deterministic ContainerId — a per-device-FAMILY tag (`"PFDS"` for the pads,
/// `"PFMO"` for the virtual mouse) so two families at the same index never share a container
/// (Windows would group them into one "device" in the Devices UI).
pub container_tag: u32,
/// Index for the deterministic per-pad ContainerId — ALSO stamped into the devnode Location,
/// which the driver reads as its bootstrap-mailbox index.
pub container_index: u8,
/// The INF-matched hardware id (`pf_dualsense` / `pf_dualshock4`), listed FIRST so the INF binds.
pub hwid: &'a str,
/// The USB VID&PID token (`VID_054C&PID_0CE6`) used to synthesize the USB hardware/compatible ids.
pub usb_vid_pid: &'a str,
/// USB composite interface number to synthesize (`&MI_xx` appended to the USB hardware ids).
/// hidclass mirrors the parent's `USB\VID…` tokens into the HID child's hardware ids, and
/// hidapi/SDL/Steam parse the child's `MI_` token as `bInterfaceNumber` (defaulting to 0 when
/// absent) — the Steam Deck's controller lives on interface 2, the gate the N4 spike hit.
pub usb_mi: Option<u8>,
/// Device description shown in Device Manager.
pub description: &'a str,
}
/// Spawn the per-session virtual controller devnode under enumerator `punktfunk` (instance
/// `profile.instance`). The returned `HSWDEVICE` owns it — `SwDeviceClose` removes it on drop, so the
/// pad appears/disappears with the session and nothing persists.
///
/// **Game-detection identity** (see `design/windows-dualsense-game-detection.md`). `HIDD_ATTRIBUTES`
/// alone (VID/PID via the IOCTL) satisfies SDL/HIDAPI/RawInput, but a native PS5 path (libScePad-
/// style raw HID) classifies the *connection type* by walking from the HID child to its parent
/// (`CM_Get_Parent`) and string-matching `"USB"`/`"BTHENUM"` in that parent's
/// `DEVPKEY_Device_CompatibleIds`; with no bus identity the pad reads as `UNKNOWN` and the native
/// path rejects it. So we set, via `SW_DEVICE_CREATE_INFO` (NOT `pProperties` — bus/identity info is
/// create-time-only and a `DEVPROPERTY` write of these keys is ignored):
/// - `pszzCompatibleIds` starting with a `USB\` token → the parent walk resolves `bus_type = USB`.
/// - `pszzHardwareIds` = `pf_dualsense` **first** (so the INF still binds our UMDF driver) followed
/// by `USB\VID_054C&PID_0CE6[&REV_0100]`, which makes hidclass derive the real-DualSense child
/// hardware ids `HID\VID_054C&PID_0CE6[&REV_0100]` (the set a genuine USB DS5 exposes).
/// - a deterministic, non-sentinel per-pad `pContainerId` (groups the pad's devnodes; avoids the
/// null-sentinel ContainerId that trips an `xinput1_4` slot-skip bug).
///
/// (Validated live on `.173`: the INF still binds, the child gains the `HID\VID&PID` ids, and the
/// parent walk reports USB. Remaining gap: GameInput parses VID/PID from the child *instance path*
/// `HID\punktfunk\…`, which only a real USB-bus instance path — a bus driver — would change.)
///
/// Two requirements each yield E_INVALIDARG if violated: the enumerator name must not contain `_`
/// (hence `punktfunk`, not `pf_dualsense`), and the completion callback is mandatory (the docs mark
/// `pCallback` as `[in]`, not optional — a NULL callback is rejected). The caller must be
/// Administrator (the host service runs as LocalSystem).
pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<String>)> {
// Build a double-NUL-terminated UTF-16 multi-sz from a list of ids.
let multi_sz = |ids: &[&str]| -> Vec<u16> {
ids.iter()
.flat_map(|s| s.encode_utf16().chain(std::iter::once(0)))
.chain(std::iter::once(0))
.collect()
};
let mi = p.usb_mi.map(|n| format!("&MI_{n:02}")).unwrap_or_default();
let usb_rev = format!("USB\\{}&REV_0100{mi}", p.usb_vid_pid);
let usb = format!("USB\\{}{mi}", p.usb_vid_pid);
let hwids = multi_sz(&[
p.hwid, // FIRST → the INF binds our UMDF driver on this id
usb_rev.as_str(),
usb.as_str(),
]);
let compat = multi_sz(&[
usb.as_str(), // a `USB\` token → native bus-type detection resolves USB
"USB\\Class_03&SubClass_00&Prot_00",
"USB\\Class_03",
]);
let instid: Vec<u16> = p
.instance
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let desc: Vec<u16> = p
.description
.encode_utf16()
.chain(std::iter::once(0))
.collect();
// The pad index, stamped into the device Location — the driver reads it to poll `pfds-boot-<index>`
// (multi-pad). The buffer outlives the SwDeviceCreate call (we wait on the event before return).
let loc: Vec<u16> = format!("{}", p.container_index)
.encode_utf16()
.chain(std::iter::once(0))
.collect();
// Deterministic ContainerId {<tag>-0000-0000-0000-0000000000<idx>} (tag e.g. "PFDS"/"PFMO").
let container = GUID::from_values(
p.container_tag,
0x0000,
0x0000,
[0, 0, 0, 0, 0, 0, 0, p.container_index],
);
// SAFETY: zeroed then the fields we use are set; cbSize identifies the struct version. The id
// buffers and `container` outlive the SwDeviceCreate call (we wait on the event before return).
let mut info: SW_DEVICE_CREATE_INFO = unsafe { std::mem::zeroed() };
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.pszzCompatibleIds = PCWSTR(compat.as_ptr());
info.pContainerId = &container;
info.pszDeviceDescription = PCWSTR(desc.as_ptr());
info.pszDeviceLocation = PCWSTR(loc.as_ptr());
info.CapabilityFlags = 0x0000_000B; // DriverRequired | SilentInstall | Removable
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
// `result` starts as E_FAIL, NOT S_OK: if the wait below times out, a zero-initialised HRESULT
// would read as success and mask the failure (found by the 2026-07 driver-health audit).
let mut ctx = SwCreateCtx {
event,
result: E_FAIL,
instance_id: [0; 128],
};
// 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.
let hsw = match unsafe {
SwDeviceCreate(
w!("punktfunk"),
w!("HTREE\\ROOT\\0"),
&info,
None,
Some(sw_create_cb),
Some(&mut ctx as *mut SwCreateCtx as *const c_void),
)
} {
Ok(h) => h,
Err(e) => {
// SAFETY: event is valid.
unsafe {
let _ = CloseHandle(event);
}
return Err(anyhow!("SwDeviceCreate failed: {e}"));
}
};
// Block until PnP finishes enumerating (the callback signals), then check its result.
// SAFETY: event is valid.
let wait = unsafe { WaitForSingleObject(event, 10_000) };
// SAFETY: event is valid.
unsafe {
let _ = CloseHandle(event);
}
if wait != WAIT_OBJECT_0 {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate enumeration callback never fired (10s) — PnP may be wedged"
));
}
if ctx.result.is_err() {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate enumeration failed: {:?}",
ctx.result
));
}
Ok((hsw, ctx.instance_id()))
}
/// The identity a [`DsWinPad`] enumerates with — the plain DualSense or the Edge share the whole
/// transport (section layout, input report shape, output parse); only the `device_type` stamp and
/// the PnP identity differ. The DS4 differs in report codec too, so it keeps its own pad type.
pub(super) struct WinDsIdentity {
/// `device_type` stamped into the section (the driver picks its HID identity off it).
pub devtype: u8,
/// PnP instance-id prefix (`pf_pad` / `pf_edge`) — distinct namespaces per type.
pub instance_prefix: &'static str,
/// The INF-matched hardware id.
pub hwid: &'static str,
/// The USB VID&PID token for the synthesized bus identity.
pub usb_vid_pid: &'static str,
/// Device Manager description.
pub description: &'static str,
}
impl WinDsIdentity {
pub(super) const fn dualsense() -> WinDsIdentity {
WinDsIdentity {
devtype: 0,
instance_prefix: "pf_pad",
hwid: "pf_dualsense",
usb_vid_pid: "VID_054C&PID_0CE6",
description: "punktfunk Virtual DualSense",
}
}
pub(super) const fn dualsense_edge() -> WinDsIdentity {
WinDsIdentity {
devtype: DEVTYPE_DUALSENSE_EDGE,
instance_prefix: "pf_edge",
hwid: "pf_dualsenseedge",
usb_vid_pid: "VID_054C&PID_0DF2",
description: "punktfunk Virtual DualSense Edge",
}
}
}
impl DsWinPad {
/// Create the sealed channel (unnamed DATA section + `Global\pfds-boot-<index>` mailbox), stamp
/// the device type FIRST (so it's visible the moment magic is) + the pad index + a neutral
/// report + the magic LAST, then spawn the devnode (the driver loads on it and receives the
/// DATA handle over the bootstrap). The devnode lives for the pad's lifetime — dropping the pad
/// removes it (`SwDeviceClose`).
pub(super) fn open(index: u8, id: &WinDsIdentity) -> Result<DsWinPad> {
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
unsafe {
*base.add(OFF_DEVTYPE) = id.devtype;
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS_INPUT_REPORT_LEN], {
let mut r = [0u8; DS_INPUT_REPORT_LEN];
serialize_state(&mut r, &DsState::neutral(), 0, 0);
r
});
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
// Spawn the per-session devnode via SwDeviceCreate; `SwDeviceClose` removes it on drop. On the
// rare failure we keep the section + data plane and fall back to an out-of-band devnode
// (installer / dev-box devgen) — its persistent driver polls the same mailbox name.
let inst = format!("{}_{index}", id.instance_prefix);
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: id.hwid,
usb_vid_pid: id.usb_vid_pid,
usb_mi: None, // single-interface USB devices (real DS/Edge have no MI_ token)
description: id.description,
}) {
Ok((h, i)) => (Some(h), i),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), hwid = id.hwid, "SwDeviceCreate failed; falling back to an out-of-band devnode");
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
// Bounded eager delivery so the driver holds the DATA section before hidclass asks it for
// descriptors (the driver reads `device_type` from the section to pick its HID identity).
channel.deliver_eager(Duration::from_millis(1500));
Ok(DsWinPad {
_sw,
channel,
attach: super::gamepad_raii::DriverAttach::new(
id.hwid,
"pf_dualsense.inf", // one driver package serves every PS identity
"C:\\Users\\Public\\pfds-driver.log",
boot_name,
instance_id,
),
seq: 0,
ts: 0,
last_out_seq: 0,
})
}
/// Serialize `st` into report `0x01` and publish it to the section's input slot.
pub(super) fn write_state(&mut self, st: &DsState) {
self.seq = self.seq.wrapping_add(1);
self.ts = self.ts.wrapping_add(1);
let mut r = [0u8; DS_INPUT_REPORT_LEN];
serialize_state(&mut r, st, self.seq, self.ts);
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64. Unlike the
// XUSB `packet` / DualSense `out_seq` fields, the input path has NO driver-polled change-detect
// field to publish last: the `pf_dualsense` driver streams the whole `input` region to game
// READ_REPORTs on its ~125 Hz timer, and the report's own sequence counter (r[7], mid-report)
// is consumed by the game's HID stack, not the driver — so it cannot serve as a separable
// publish flag without a seqlock generation the driver `Acquire`-reads (a `PadShm` layout +
// driver change, deferred). The `Release` fence after the copy orders the report-body stores
// ahead of this pad's next `Release` publish (the bootstrap/seq stores in `channel.pump()`),
// giving the copy Release visibility on a weakly-ordered core (ARM64); on x86-TSO it is a
// no-op. Residual: absent a driver-side `Acquire` on a per-frame input generation, a torn
// single frame is still theoretically possible but self-heals on the next ~250 Hz write.
unsafe {
std::ptr::copy_nonoverlapping(
r.as_ptr(),
self.channel.data_base().add(OFF_INPUT),
r.len(),
);
fence(Ordering::Release);
};
}
/// Poll the section's output slot; parse a new `0x02` report (rumble / LEDs / triggers) into a
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything
/// new. Also ticks the sealed-channel delivery and feeds the driver-attach health watcher (the
/// driver's ~125 Hz timer stamps `driver_proto` while it has the section mapped).
pub(super) fn service(&mut self, pad: u8) -> DsFeedback {
self.channel.pump();
let mut fb = DsFeedback::default();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
};
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes; `OFF_OUT_SEQ` (== 72) is 4-aligned off the
// page-aligned base, so the `AtomicU32` view is valid. The driver bumps `out_seq` AFTER
// writing the `output` report, so an `Acquire` load here orders the `output` copy below after
// it — a fresh seq guarantees a coherent snapshot of the output bytes on a weakly-ordered core
// (ARM64). On x86-TSO it is a plain load.
let seq = unsafe {
(*(self.channel.data_base().add(OFF_OUT_SEQ) as *const AtomicU32))
.load(Ordering::Acquire)
};
if seq != self.last_out_seq {
self.last_out_seq = seq;
fb.fresh = true;
let mut out = [0u8; 64];
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
unsafe {
std::ptr::copy_nonoverlapping(
self.channel.data_base().add(OFF_OUTPUT),
out.as_mut_ptr(),
64,
)
};
parse_ds_output(pad, &out, &mut fb);
}
fb
}
}
/// The Windows-DualSense half of the shared stateful manager (see [`PadProto`]): the UMDF
/// sealed-channel open, the same [`DsState`] mappers as `linux/dualsense.rs`, and the section
/// feedback poll. Lifecycle (slot table, unplug sweep, heartbeat, dedup) lives in [`UhidManager`].
pub struct DsWinProto {
/// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button
/// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualsense.rs`.
remap: crate::steam_remap::RemapConfig,
}
impl Default for DsWinProto {
fn default() -> DsWinProto {
DsWinProto {
remap: crate::steam_remap::RemapConfig::from_env(),
}
}
}
impl PadProto for DsWinProto {
type Pad = DsWinPad;
type State = DsState;
const LABEL: &'static str = "DualSense/Windows";
const DEVICE: &'static str = "DualSense";
const CREATE_HINT: &'static str =
" (install/repair: punktfunk-host.exe driver install --gamepad)";
fn open(&mut self, idx: u8) -> Result<DsWinPad> {
let p = DsWinPad::open(idx, &WinDsIdentity::dualsense())?;
tracing::info!(
index = idx,
"virtual DualSense created (Windows UMDF shm channel)"
);
Ok(p)
}
fn neutral(&self) -> DsState {
DsState::neutral()
}
/// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (rich-
/// plane fields that must survive a button-only frame) — exactly as `linux/dualsense.rs` does.
fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState {
// Steam back grips have no DualSense slot — fold them onto standard buttons per the
// configured policy (default drop) so they aren't silently lost.
let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles);
let mut s = DsState::from_gamepad(
buttons,
f.ls_x,
f.ls_y,
f.rs_x,
f.rs_y,
f.left_trigger,
f.right_trigger,
);
s.touch = prev.touch;
s.gyro = prev.gyro;
s.accel = prev.accel;
s.touch_click = prev.touch_click;
s
}
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
/// split the one touchpad left/right, pad clicks ride touch_click.
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
st.apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
}
fn write_state(&self, pad: &mut DsWinPad, st: &DsState) {
pad.write_state(st);
}
/// Poll the section for a game's feedback: motor rumble on the universal 0xCA plane, the rich
/// lightbar/player-LED/trigger events on the 0xCD plane.
fn service(&self, pad: &mut DsWinPad, idx: u8) -> PadFeedback {
let fb = pad.service(idx);
PadFeedback {
rumble: fb.rumble,
hidout: fb.hidout,
game_drove: Some(fb.fresh),
}
}
}
/// **N4 spike** (gamepad-new-types §6, timeboxed): create a software-devnode HID **Steam Deck**
/// (`device_type = 3`, `VID_28DE&PID_1205`) and hold it for `secs`, streaming the neutral Deck
/// frame, so the go/no-go question — does Steam Input on Windows promote a software-devnode HID
/// Deck, or does it require a real USB bus identity (the documented GameInput instance-path
/// gap)? — can be answered by watching Steam's `logs/controller.txt` / controller settings
/// while this holds. Never used by a session; wired to the `deck-windows-spike` subcommand.
pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
let mut channel = PadChannel::create(boot_name, SHM_SIZE)?;
let base = channel.data_base();
// Neutral Deck input frame: [0x01, 0x00, ID_CONTROLLER_DECK_STATE=0x09, 0x3C], all released.
let mut neutral = [0u8; 64];
(neutral[0], neutral[2], neutral[3]) = (0x01, 0x09, 0x3C);
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range. Device-type
// FIRST, magic LAST — the same publish order the session pads use.
unsafe {
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_STEAMDECK;
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; 64], neutral);
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let inst = format!("pf_deckspike_{index}");
let (hsw, _) = create_swdevice(&SwDeviceProfile {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_steamdeck",
usb_vid_pid: "VID_28DE&PID_1205",
// The Deck's controller interface — the promotion gate the first spike run hit
// (hidapi parses MI_ from the child hwids; absent = interface 0, Steam wants 2).
usb_mi: Some(2),
description: "punktfunk Virtual Steam Deck (spike)",
})?;
let _sw = super::gamepad_raii::SwDevice::new(hsw);
channel.deliver_eager(std::time::Duration::from_millis(1500));
println!(
"virtual Steam Deck devnode up (28DE:1205, device_type 3) — holding {secs}s.\n\
Observe: Get-PnpDevice -PresentOnly | findstr 1205; Steam logs\\controller.txt for a\n\
detect/promote line; Steam Settings > Controller for a 'Steam Deck' entry.\n\
GO = Steam lists/promotes it; NO-GO = it never appears (the Linux `Interface: -1` gap\n\
applies verbatim — document and keep the SteamDeck->DualSense Windows fold)."
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);
let mut last_out_seq = 0u32;
while std::time::Instant::now() < deadline {
channel.pump();
// Log any feature/output traffic Steam sends — each one is spike evidence.
// SAFETY: base points at SHM_SIZE bytes; OFF_OUT_SEQ is in range.
let seq =
unsafe { std::ptr::read_unaligned(channel.data_base().add(OFF_OUT_SEQ) as *const u32) };
if seq != last_out_seq {
last_out_seq = seq;
let mut out = [0u8; 16];
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
unsafe {
std::ptr::copy_nonoverlapping(
channel.data_base().add(OFF_OUTPUT),
out.as_mut_ptr(),
16,
)
};
println!(" output report from a client (Steam?): {out:02x?}");
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
println!("deck-windows-spike: done (devnode removed on exit)");
Ok(())
}
/// All virtual DualSense pads of a session — the Windows analogue of
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface (via the shared
/// [`UhidManager`]) so the session input thread drives either backend identically. The heartbeat
/// keeps the section fresh (the driver's timer streams whatever's in it) — parity with the UHID
/// backend's silence heartbeat.
pub type DualSenseWindowsManager = UhidManager<DsWinProto>;
@@ -0,0 +1,241 @@
//! Virtual Sony DualShock 4 on Windows via the UMDF minidriver — the PS4 sibling of
//! [`super::dualsense_windows`]. Same transport (a per-session `SwDeviceCreate` devnode + the sealed
//! shared-memory channel bootstrapped via `Global\pfds-boot-<idx>`), same controller model
//! ([`DsState`]); only the PnP identity (`VID_054C&PID_09CC`, hardware id `pf_dualshock4`) and the
//! report codec ([`super::dualshock4_proto`]) differ. The host stamps `device_type = 1` (DualShock 4)
//! into the DATA section so the one UMDF driver serves the DS4 descriptor / attributes / features
//! instead of the DualSense ones. Feedback is motor rumble (universal 0xCA plane) + the lightbar
//! (0xCD `Led`); a DS4 has no adaptive triggers / player LEDs.
use super::dualsense_proto::DsState;
use super::dualsense_windows::{
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT,
OFF_OUTPUT, OFF_OUT_SEQ, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE,
};
use super::dualshock4_proto::{
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
};
use super::gamepad_raii::PadChannel;
use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::Result;
use punktfunk_core::quic::{HidOutput, RichInput};
use std::time::Duration;
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the sealed
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait), like the
/// Linux pads.
pub struct Ds4WinPad {
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
_sw: Option<super::gamepad_raii::SwDevice>,
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
channel: PadChannel,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
counter: u8,
ts: u16,
last_out_seq: u32,
}
impl Ds4WinPad {
/// Create the sealed channel, stamp `device_type = DualShock 4` + the pad index + a neutral
/// report + the magic LAST, then spawn the `pf_ds4_<index>` devnode (the driver loads on it and
/// receives the DATA handle over the bootstrap).
fn open(index: u8) -> Result<Ds4WinPad> {
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
// device-type FIRST (so it's visible the moment magic is), pad index, neutral report,
// magic LAST.
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
unsafe {
*base.add(OFF_DEVTYPE) = DEVTYPE_DUALSHOCK4;
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS4_INPUT_REPORT_LEN], {
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
serialize_state(&mut r, &DsState::neutral(), 0, 0);
r
});
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let inst = format!("pf_ds4_{index}");
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_dualshock4",
usb_vid_pid: "VID_054C&PID_09CC",
usb_mi: None,
description: "punktfunk Virtual DualShock 4",
}) {
Ok((h, id)) => (Some(h), id),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; DualShock 4 devnode unavailable");
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
// Bounded eager delivery — for the DS4 this is what closes the identity race: the driver
// must read `device_type = 1` from the delivered DATA section before hidclass asks it for
// descriptors, or the pad would enumerate with the (default) DualSense identity.
channel.deliver_eager(Duration::from_millis(1500));
Ok(Ds4WinPad {
_sw,
channel,
attach: super::gamepad_raii::DriverAttach::new(
"pf_dualshock4",
"pf_dualsense.inf", // one driver package serves both HID identities
"C:\\Users\\Public\\pfds-driver.log",
boot_name,
instance_id,
),
counter: 0,
ts: 0,
last_out_seq: 0,
})
}
/// Serialize `st` into report `0x01` and publish it to the section's input slot.
fn write_state(&mut self, st: &DsState) {
self.counter = self.counter.wrapping_add(1);
self.ts = self.ts.wrapping_add(188); // ~1ms in the DS4's 5.33µs sensor-clock units
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
serialize_state(&mut r, st, self.counter, self.ts);
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
unsafe {
std::ptr::copy_nonoverlapping(
r.as_ptr(),
self.channel.data_base().add(OFF_INPUT),
r.len(),
)
};
}
/// Poll the section's output slot; parse a new `0x05` report (rumble / lightbar) into a
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new. Also
/// ticks the sealed-channel delivery and feeds the driver-attach health watcher (the driver's
/// ~125 Hz timer stamps `driver_proto`).
fn service(&mut self) -> Ds4Feedback {
self.channel.pump();
let mut fb = Ds4Feedback::default();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
};
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes.
let seq = unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
};
if seq != self.last_out_seq {
self.last_out_seq = seq;
fb.fresh = true;
let mut out = [0u8; 64];
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
unsafe {
std::ptr::copy_nonoverlapping(
self.channel.data_base().add(OFF_OUTPUT),
out.as_mut_ptr(),
64,
)
};
parse_ds4_output(&out, &mut fb);
}
fb
}
}
/// The Windows-DualShock-4 half of the shared stateful manager (see [`PadProto`]): the UMDF
/// sealed-channel open (device-type 1), the same [`DsState`] mappers as `linux/dualshock4.rs`, and
/// the section feedback poll. Lifecycle (slot table, unplug sweep, heartbeat, dedup) lives in
/// [`UhidManager`]; the lightbar dedup that used to be a bespoke `last_led` vec now rides the
/// shared `HidoutDedup` (identical semantics — `Led` is compared against the last-forwarded value
/// and re-armed on create/unplug).
pub struct Ds4WinProto {
/// Fallback policy for the Steam back grips a client may send (the DS4 has no back-button HID
/// slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualshock4.rs`.
remap: crate::steam_remap::RemapConfig,
}
impl Default for Ds4WinProto {
fn default() -> Ds4WinProto {
Ds4WinProto {
remap: crate::steam_remap::RemapConfig::from_env(),
}
}
}
impl PadProto for Ds4WinProto {
type Pad = Ds4WinPad;
type State = DsState;
const LABEL: &'static str = "DualShock 4/Windows";
const DEVICE: &'static str = "DualShock 4";
const CREATE_HINT: &'static str =
" (install/repair: punktfunk-host.exe driver install --gamepad)";
fn open(&mut self, idx: u8) -> Result<Ds4WinPad> {
let p = Ds4WinPad::open(idx)?;
tracing::info!(
index = idx,
"virtual DualShock 4 created (Windows UMDF shm channel)"
);
Ok(p)
}
fn neutral(&self) -> DsState {
DsState::neutral()
}
/// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (rich-
/// plane fields that must survive a button-only frame) — exactly as `linux/dualshock4.rs` does.
fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState {
// Steam back grips have no DS4 slot — fold them onto standard buttons per the configured
// policy (default drop) so they aren't silently lost.
let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles);
let mut s = DsState::from_gamepad(
buttons,
f.ls_x,
f.ls_y,
f.rs_x,
f.rs_y,
f.left_trigger,
f.right_trigger,
);
s.touch = prev.touch;
s.gyro = prev.gyro;
s.accel = prev.accel;
s.touch_click = prev.touch_click;
s
}
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
/// split the one touchpad left/right, pad clicks ride touch_click.
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
st.apply_rich(rich, DS4_TOUCH_W, DS4_TOUCH_H);
}
fn write_state(&self, pad: &mut Ds4WinPad, st: &DsState) {
pad.write_state(st);
}
/// Poll the section for a game's feedback: motor rumble on the universal 0xCA plane, the
/// lightbar as a 0xCD `Led` event (a DS4 has no player LEDs / adaptive triggers).
fn service(&self, pad: &mut Ds4WinPad, idx: u8) -> PadFeedback {
let fb = pad.service();
PadFeedback {
rumble: fb.rumble,
hidout: fb
.led
.map(|(r, g, b)| HidOutput::Led { pad: idx, r, g, b })
.into_iter()
.collect(),
game_drove: Some(fb.fresh),
}
}
}
/// All virtual DualShock 4 pads of a session — the Windows analogue of
/// [`DualShock4Manager`](super::dualshock4::DualShock4Manager), with the same method surface (via
/// the shared [`UhidManager`]) as the Windows DualSense manager so the session input thread drives
/// either backend identically.
pub type DualShock4WindowsManager = UhidManager<Ds4WinProto>;
@@ -0,0 +1,714 @@
//! Per-pad Windows resource RAII + the **sealed gamepad channel** broker (DualSense / DualShock 4 /
//! XUSB backends).
//!
//! Each virtual pad owns three OS resources: the **unnamed** DATA section the `pf_dualsense`/`pf_xusb`
//! driver works against (`XusbShm`/`PadShm`), the tiny **named** bootstrap mailbox
//! (`pf_driver_proto::gamepad::PadBootstrap`) that hands the driver a duplicated handle to it, and the
//! `SwDeviceCreate`'d software devnode the driver loads on. [`Shm`] and [`SwDevice`] own the resources
//! with RAII; [`PadChannel`] owns the two sections plus the delivery handshake.
//!
//! **Why the channel is sealed** (`design/gamepad-channel-sealing.md`): the DATA section used to be a
//! `Global\pf…-shm-<index>` named section with an SY+LS DACL, which let any *sibling LocalService*
//! process open it by name to read the live controller input or inject/forge input and rumble — the
//! same name-open vector the frame ring closed (`design/idd-push-security.md`). The DATA section is now
//! UNNAMED with a SYSTEM-only DACL and reaches the driver exclusively as a handle this host duplicated
//! into its WUDFHost (a duplicated handle carries the source's access, so no LS ACE is needed). The pad
//! drivers are UMDF HID minidrivers with **no control device** (hidclass owns the stack), so unlike the
//! frame channel there is no IOCTL to deliver the handle or learn the WUDFHost pid — hence the
//! late-bound [`PadBootstrap`] mailbox handshake, the one *named* object left. It carries only pids and
//! a handle VALUE (meaningless outside the target process), so tampering with it yields at worst a
//! gamepad DoS, never a read or an injection; the empirical floor from the frame work holds here too
//! (a LocalService token is DACL-denied `OpenProcess` on a UMDF WUDFHost for every access right).
use anyhow::{anyhow, bail, Context, Result};
use pf_driver_proto::gamepad::{PadBootstrap, BOOT_MAGIC, GAMEPAD_PROTO_VERSION};
use std::ffi::c_void;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
use std::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use windows::core::{w, HRESULT, HSTRING, PCWSTR};
use windows::Win32::Devices::DeviceAndDriverInstallation::{
CM_Get_DevNode_Status, CM_Locate_DevNodeW, CM_DEVNODE_STATUS_FLAGS, CM_LOCATE_DEVNODE_NORMAL,
CM_PROB, CR_SUCCESS, DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED,
};
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
use windows::Win32::Foundation::{
DuplicateHandle, GetLastError, LocalFree, SetLastError, DUPLICATE_HANDLE_OPTIONS,
ERROR_ALREADY_EXISTS, HANDLE, HLOCAL, INVALID_HANDLE_VALUE, WIN32_ERROR,
};
use windows::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
use windows::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
use windows::Win32::System::Memory::{
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS,
MEMORY_MAPPED_VIEW_ADDRESS, PAGE_READWRITE,
};
use windows::Win32::System::Threading::{
GetCurrentProcess, OpenProcess, SetEvent, PROCESS_DUP_HANDLE, PROCESS_QUERY_LIMITED_INFORMATION,
};
/// Least access the pad driver needs on the duplicated DATA section: it only MAPS it read/write, so
/// `SECTION_MAP_READ | SECTION_MAP_WRITE` (== the driver's `FILE_MAP_RW`). Granted explicitly in
/// [`PadChannel::deliver_to`] instead of `DUPLICATE_SAME_ACCESS` (least privilege for the sealed
/// section — the driver's handle then can't take ownership / change security / delete the object).
const SECTION_MAP_RW: u32 = 0x0004 | 0x0002;
/// An anonymous (pagefile-backed) shared section + its mapped read/write view. RAII: drop unmaps the
/// view, then the [`OwnedHandle`] closes the section handle (in that order). Created either
/// [unnamed](Self::create_unnamed) (the sealed DATA section — reachable only by handle duplication) or
/// [named](Self::create_named) (the bootstrap mailbox the driver opens by name).
pub(super) struct Shm {
/// Owns the section handle (closed on drop). Also the duplication source for the sealed channel —
/// see [`Shm::raw_handle`].
handle: OwnedHandle,
view: MEMORY_MAPPED_VIEW_ADDRESS,
}
/// Owns an SDDL-derived `SECURITY_ATTRIBUTES` **and** the OS-allocated security descriptor its
/// `lpSecurityDescriptor` points at (`ConvertStringSecurityDescriptorToSecurityDescriptorW`
/// `LocalAlloc`s the descriptor). Drop `LocalFree`s it, so a `SecAttr` must outlive every
/// `CreateFileMappingW` that borrows its `sa`: the section copies the security info at create time, so
/// freeing after the create returns is safe — hence [`Shm::create_named`] builds one `SecAttr` before
/// its squat-retry loop and reuses it across attempts instead of re-allocating (and re-leaking) per
/// attempt.
struct SecAttr {
sa: SECURITY_ATTRIBUTES,
psd: PSECURITY_DESCRIPTOR,
}
impl Drop for SecAttr {
fn drop(&mut self) {
// SAFETY: `psd` is the descriptor `ConvertStringSecurityDescriptorToSecurityDescriptorW`
// allocated for us with `LocalAlloc`; release it with the matching `LocalFree`. Every
// `CreateFileMappingW` that borrowed `self.sa` has already returned (so has copied the
// security info into its section object), so no live `SECURITY_ATTRIBUTES` still points here.
unsafe {
let _ = LocalFree(Some(HLOCAL(self.psd.0)));
}
}
}
/// Build a [`SecAttr`] from an SDDL literal — a `SECURITY_ATTRIBUTES` plus the descriptor it borrows,
/// freed together on drop. The returned owner must outlive every `CreateFileMappingW` that borrows
/// its `sa` (see [`SecAttr`]).
fn sddl_sa(sddl: PCWSTR) -> Result<SecAttr> {
let mut psd = PSECURITY_DESCRIPTOR::default();
// SAFETY: the SDDL literal is valid; `psd` receives a `LocalAlloc`'d descriptor that `SecAttr`'s
// `Drop` `LocalFree`s once the section create that borrows it has returned.
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl,
SDDL_REVISION_1,
&mut psd,
None,
)?;
}
Ok(SecAttr {
sa: SECURITY_ATTRIBUTES {
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: psd.0,
bInheritHandle: false.into(),
},
psd,
})
}
impl Shm {
/// Create + zero an **unnamed** `size`-byte section, mapped read/write — the sealed DATA section.
/// SDDL `D:P(A;;GA;;;SY)` (SYSTEM-only, protected): with no name there is nothing to enumerate,
/// open, or squat, and the driver reaches it through a duplicated handle, which carries the
/// source's access without re-checking the object DACL (the exact property the frame ring
/// validated on-glass — `design/idd-push-security.md`).
pub(super) fn create_unnamed(size: usize) -> Result<Shm> {
let sa = sddl_sa(w!("D:P(A;;GA;;;SY)"))?;
// `sa` owns the descriptor and lives to the end of this fn, so it outlives the create.
Self::create_inner(&sa.sa, PCWSTR::null(), size)
.context("create unnamed gamepad DATA section")
}
/// Create + zero a **named** `size`-byte section, mapped read/write — the bootstrap mailbox. SDDL
/// `D:(A;;GA;;;SY)(A;;GA;;;LS)`: SYSTEM (this host) + LocalService (the driver's WUDFHost opens it
/// by name). Safe to leave name-openable because it carries nothing exploitable (see the module
/// docs). **Squat-checked**: `Global\` names are creatable by any service holding
/// `SeCreateGlobalPrivilege` (LocalService has it), so if the name already exists —
/// `ERROR_ALREADY_EXISTS`, meaning `CreateFileMappingW` silently *opened* a pre-existing object we
/// don't control — we close and retry briefly (our own driver holds the name for microseconds per
/// poll tick), then fail loudly rather than run the handshake through an attacker-owned (or
/// another host instance's) mailbox.
pub(super) fn create_named(name: &HSTRING, size: usize) -> Result<Shm> {
// Build the descriptor ONCE and reuse it across the squat-retry loop — it (and the OS
// allocation it owns) lives to the end of this fn, so it outlives every create below.
// `D:P` (protected) — strip any inherited ACEs so only SYSTEM + LocalService are granted,
// matching the intent of the other named objects (security-review 2026-07-17).
let sa = sddl_sa(w!("D:P(A;;GA;;;SY)(A;;GA;;;LS)"))?;
for attempt in 0..5 {
if attempt > 0 {
std::thread::sleep(Duration::from_millis(50));
}
// SAFETY: clearing the thread error slot so ERROR_ALREADY_EXISTS below is unambiguous.
unsafe { SetLastError(WIN32_ERROR(0)) };
let shm = Self::create_inner(&sa.sa, PCWSTR(name.as_ptr()), size)
.with_context(|| format!("create gamepad bootstrap mailbox {name}"))?;
// SAFETY: read immediately after the create; windows-rs only touches the error slot on
// failure, so a success here preserves CreateFileMappingW's ALREADY_EXISTS signal.
if unsafe { GetLastError() } != ERROR_ALREADY_EXISTS {
return Ok(shm);
}
// `shm` drops here → unmap + close our handle to the foreign object, then retry.
}
bail!(
"bootstrap mailbox {name} already exists and stayed alive across retries — another \
punktfunk-host instance is serving this pad index, or a local service is squatting the \
name (gamepad DoS attempt?)"
);
}
fn create_inner(sa: &SECURITY_ATTRIBUTES, name: PCWSTR, size: usize) -> Result<Shm> {
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the caller's SDDL; the
// descriptor behind `sa` outlives this call (owned by the caller's `SecAttr`, freed only once
// every create that borrows it has returned).
let map = unsafe {
CreateFileMappingW(
INVALID_HANDLE_VALUE,
Some(sa),
PAGE_READWRITE,
0,
size as u32,
name,
)?
};
// SAFETY: `map` is a fresh section handle we own; take ownership immediately so that the early
// return below (and the eventual drop) closes it. `map` (a `Copy` `HANDLE`) stays usable for the
// `MapViewOfFile` borrow that follows — `from_raw_handle` only copies the inner pointer.
let handle = unsafe { OwnedHandle::from_raw_handle(map.0) };
// SAFETY: `map` is a valid section handle; map the whole thing read/write.
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size) };
if view.Value.is_null() {
// `handle` drops here → closes the section. No view to unmap.
return Err(anyhow!("MapViewOfFile failed"));
}
// SAFETY: `view` points at `size` writable bytes (just mapped).
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
Ok(Shm { handle, view })
}
/// The mapped section's base pointer. Stable for the `Shm`'s lifetime (moving the `Shm` does not
/// relocate the OS mapping — the view address is fixed by `MapViewOfFile`).
pub(super) fn base(&self) -> *mut u8 {
self.view.Value as *mut u8
}
/// The section handle as a borrowed `HANDLE` (the sealed channel's duplication source).
fn raw_handle(&self) -> HANDLE {
HANDLE(self.handle.as_raw_handle())
}
}
impl Drop for Shm {
fn drop(&mut self) {
// SAFETY: `view` came from `MapViewOfFile`; unmap it BEFORE the `handle` field closes the
// section (struct fields drop only after this `Drop::drop` returns).
unsafe {
let _ = UnmapViewOfFile(self.view);
}
}
}
// ── The sealed-channel bootstrap broker ─────────────────────────────────────────────────────────
/// Global delivery sequence for [`PadBootstrap::handle_seq`] — host-wide monotonic and never 0, so two
/// consecutive pads on the same index can't hand the (persistent, out-of-band-devnode) driver the same
/// seq twice. Starts at 1.
static BOOT_SEQ: AtomicU32 = AtomicU32::new(1);
/// Hard cap on delivery attempts per pad: each attempt duplicates a handle into a WUDFHost, so a
/// tampered mailbox flapping `driver_pid` must not mint unbounded remote handles (DoS containment).
/// A legitimate pad needs exactly one (a driver restart within one pad lifetime is not a thing —
/// the WUDFHost dies with the devnode).
const MAX_DELIVERY_ATTEMPTS: u32 = 16;
/// One pad's sealed host↔driver channel: the unnamed DATA section (the real `XusbShm`/`PadShm`), the
/// named bootstrap mailbox, and the delivery state machine ([`Self::pump`]) that hands the driver's
/// WUDFHost a duplicated DATA handle once it publishes its pid. Owns both sections (RAII teardown —
/// dropping the channel closes the mailbox, whose *name* then disappears, which is how a persistent
/// (out-of-band-devnode) driver detects the host is gone).
pub(super) struct PadChannel {
data: Shm,
boot: Shm,
boot_name: String,
/// Last `driver_pid` acted on (delivered or rejected) — never retry the same value, so a failed
/// verify can't be spun into a hot loop by a static mailbox.
last_seen_pid: u32,
attempts: u32,
delivered: bool,
warned_proto: bool,
warned_cap: bool,
}
impl PadChannel {
/// Create the unnamed DATA section (`data_size` bytes, zeroed — the caller stamps its layout and
/// magic) plus the named bootstrap mailbox, stamped `host_proto` first and `BOOT_MAGIC` last so a
/// driver only trusts a fully-initialized mailbox.
pub(super) fn create(boot_name: String, data_size: usize) -> Result<PadChannel> {
let data = Shm::create_unnamed(data_size)?;
let boot = Shm::create_named(
&HSTRING::from(boot_name.as_str()),
core::mem::size_of::<PadBootstrap>(),
)?;
let base = boot.base();
// SAFETY: `base` is the live, page-aligned mailbox view (>= size_of::<PadBootstrap>()); the
// field offsets are pinned by the proto's asserts and naturally aligned, so the atomic views
// are valid. `host_proto` is published BEFORE `magic` (Release) — a driver that observes the
// magic (Acquire) sees the version.
unsafe {
(*(base.add(core::mem::offset_of!(PadBootstrap, host_proto)) as *const AtomicU32))
.store(GAMEPAD_PROTO_VERSION, Ordering::Relaxed);
fence(Ordering::Release);
(*(base.add(core::mem::offset_of!(PadBootstrap, magic)) as *const AtomicU32))
.store(BOOT_MAGIC, Ordering::Release);
}
Ok(PadChannel {
data,
boot,
boot_name,
last_seen_pid: 0,
attempts: 0,
delivered: false,
warned_proto: false,
warned_cap: false,
})
}
/// The DATA section's mapped base (the host side of `XusbShm`/`PadShm`).
pub(super) fn data_base(&self) -> *mut u8 {
self.data.base()
}
/// The bootstrap mailbox name (log labelling).
pub(super) fn boot_name(&self) -> &str {
&self.boot_name
}
/// Atomic `u32` load from a mailbox field.
fn boot_load(&self, off: usize) -> u32 {
// SAFETY: the mailbox view is live (owned by `self.boot`), page-aligned, and every
// `PadBootstrap` u32 field offset is 4-aligned (proto asserts), so the atomic view is valid;
// no reference into the shared region outlives the load.
unsafe { (*(self.boot.base().add(off) as *const AtomicU32)).load(Ordering::Acquire) }
}
/// One tick of the delivery state machine — called from the pad's regular service pump (≤4 ms
/// cadence) and from [`Self::deliver_eager`]. Cheap when idle: two atomic loads.
pub(super) fn pump(&mut self) {
// Version diagnostics: the driver writes its own proto version even when it refuses to
// publish a pid (host/driver mismatch), so the operator sees WHY the pad never attaches.
let drv_proto = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_proto));
if drv_proto != 0 && drv_proto != GAMEPAD_PROTO_VERSION && !self.warned_proto {
self.warned_proto = true;
tracing::warn!(
mailbox = %self.boot_name,
driver_proto = drv_proto,
host_proto = GAMEPAD_PROTO_VERSION,
"gamepad driver/host protocol mismatch on the bootstrap mailbox — update the \
drivers: punktfunk-host.exe driver install --gamepad"
);
}
let pid = self.boot_load(core::mem::offset_of!(PadBootstrap, driver_pid));
if pid == 0 || pid == self.last_seen_pid {
return;
}
self.last_seen_pid = pid;
if self.attempts >= MAX_DELIVERY_ATTEMPTS {
if !self.warned_cap {
self.warned_cap = true;
tracing::warn!(
mailbox = %self.boot_name,
attempts = self.attempts,
"gamepad channel delivery cap reached — the bootstrap mailbox keeps changing \
its driver pid (tampering?); no further handles will be duplicated"
);
}
return;
}
self.attempts += 1;
match self.deliver_to(pid) {
Ok(seq) => {
self.delivered = true;
tracing::info!(
mailbox = %self.boot_name,
wudf_pid = pid,
seq,
"sealed gamepad channel delivered (DATA handle duplicated into the driver's \
WUDFHost)"
);
}
Err(e) => {
tracing::warn!(
mailbox = %self.boot_name,
pid,
error = %format!("{e:#}"),
"sealed gamepad channel delivery failed — will retry when the mailbox reports \
a different driver pid"
);
}
}
}
/// Duplicate the DATA section into `pid`'s handle table (after verifying it is a genuine
/// WUDFHost) and publish the handle value + owning pid, bumping `handle_seq` LAST. The driver
/// adopts the handle by consuming the delivery; an unconsumed duplicate dies with the target
/// process (nothing to reap — there is no fallible step after the duplication).
fn deliver_to(&self, pid: u32) -> Result<u32> {
// SAFETY: plain FFI; the handle (checked by `?`) is owned solely here and moved into the
// `OwnedHandle` (single owner, closes on drop); `verify_is_wudfhost` borrows it for the
// synchronous check and forms no lasting alias.
let process = unsafe {
let h = OpenProcess(
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION,
false,
pid,
)
.context("OpenProcess(PROCESS_DUP_HANDLE) on the mailbox-reported pid")?;
let process = OwnedHandle::from_raw_handle(h.0 as _);
pf_capture::verify_is_wudfhost(
HANDLE(process.as_raw_handle()),
pid,
"gamepad-channel",
)?;
process
};
let mut remote = HANDLE::default();
// SAFETY: `self.data.raw_handle()` is the live section handle this channel owns;
// `process` is the live PROCESS_DUP_HANDLE target; `&mut remote` is a valid out-param.
// Least privilege: the pad driver only MAPS the DATA section read/write (its `FILE_MAP_RW` =
// `SECTION_MAP_READ | SECTION_MAP_WRITE`), so grant exactly that instead of copying our
// full-access creator handle via `DUPLICATE_SAME_ACCESS` (Chen: don't over-grant unnamed
// shared objects — a compromised driver's handle then can't `WRITE_DAC`/`DELETE` the section).
unsafe {
DuplicateHandle(
GetCurrentProcess(),
self.data.raw_handle(),
HANDLE(process.as_raw_handle()),
&mut remote,
SECTION_MAP_RW,
false,
DUPLICATE_HANDLE_OPTIONS(0),
)
.context("DuplicateHandle(gamepad DATA section) into the driver's WUDFHost")?;
}
let value = remote.0 as usize as u64;
let base = self.boot.base();
let seq = BOOT_SEQ.fetch_add(1, Ordering::Relaxed);
// SAFETY: live, page-aligned mailbox view; `data_handle` is 8-aligned and `handle_pid`/
// `handle_seq` 4-aligned (proto asserts). The handle value + owning pid are published BEFORE
// the seq (Release) — a driver that observes the new seq (Acquire) sees a complete delivery.
unsafe {
(*(base.add(core::mem::offset_of!(PadBootstrap, data_handle)) as *const AtomicU64))
.store(value, Ordering::Relaxed);
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_pid)) as *const AtomicU32))
.store(pid, Ordering::Relaxed);
fence(Ordering::Release);
(*(base.add(core::mem::offset_of!(PadBootstrap, handle_seq)) as *const AtomicU32))
.store(seq, Ordering::Release);
}
Ok(seq)
}
/// Bounded wait at pad-open: pump until the mailbox produces a driver pid we act on (delivered or
/// rejected) or `timeout` passes. Closes the identity race for the DualShock 4 (the driver reads
/// `device_type` from the DATA section when hidclass asks for descriptors — the channel should be
/// attached by then); the regular service pump takes over afterwards either way.
pub(super) fn deliver_eager(&mut self, timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
self.pump();
if self.last_seen_pid != 0 || Instant::now() >= deadline {
if !self.delivered {
tracing::debug!(
mailbox = %self.boot_name,
"eager gamepad-channel delivery window passed without an attach — the \
service pump keeps polling (driver-attach diagnosis follows if it stays \
silent)"
);
}
return;
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
/// Context for the `SwDeviceCreate` completion callback: an event to signal, the HRESULT it reports,
/// and the PnP instance id PnP assigned (captured for devnode health diagnostics). Shared by every
/// Windows companion backend (XUSB / DualSense / DS4): each `create_swdevice` builds one, hands it to
/// `SwDeviceCreate` alongside [`sw_create_cb`], and reads [`instance_id`](Self::instance_id) once the
/// callback has signalled.
#[repr(C)]
pub(super) struct SwCreateCtx {
pub(super) event: HANDLE,
pub(super) result: HRESULT,
pub(super) instance_id: [u16; 128],
}
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result and wake the
/// creator, which blocks on the event (so there's no concurrent access to `*ctx`).
pub(super) unsafe extern "system" fn sw_create_cb(
_dev: HSWDEVICE,
result: HRESULT,
ctx: *const c_void,
id: PCWSTR,
) {
if !ctx.is_null() {
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback (the
// creator blocks on the event). `id` is a NUL-terminated string for the callback's duration.
unsafe {
let c = ctx as *mut SwCreateCtx;
(*c).result = result;
if !id.is_null() {
for i in 0..(*c).instance_id.len() - 1 {
let ch = *id.0.add(i);
(*c).instance_id[i] = ch;
if ch == 0 {
break;
}
}
}
let _ = SetEvent((*c).event);
}
}
}
impl SwCreateCtx {
pub(super) fn instance_id(&self) -> Option<String> {
let len = self.instance_id.iter().position(|&c| c == 0)?;
(len > 0).then(|| String::from_utf16_lossy(&self.instance_id[..len]))
}
}
/// A `SwDeviceCreate`'d software devnode; drop removes it via `SwDeviceClose`. Replaces the manual
/// `SwDeviceClose` each backend used to call in its `Drop`.
pub(super) struct SwDevice(HSWDEVICE);
impl SwDevice {
pub(super) fn new(hsw: HSWDEVICE) -> Self {
SwDevice(hsw)
}
}
impl Drop for SwDevice {
fn drop(&mut self) {
// SAFETY: `self.0` is the handle `SwDeviceCreate` returned; `SwDeviceClose` removes the devnode.
unsafe { SwDeviceClose(self.0) };
}
}
// ── Driver health surfacing ─────────────────────────────────────────────────────────────────────
//
// The gamepad drivers have no IOCTL plane (hidclass gates the stack), so the only cross-process
// signal is the shared section itself. The drivers stamp `driver_proto` into their section once
// attached (pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION); [`DriverAttach`] watches that field
// from the host's regular pump and turns silence into actionable WARN/ERROR log lines — the piece
// that used to be missing entirely: a pad could be "created" with no driver installed and nothing
// was ever logged until the user gave up ("host doesn't see my controller" bug reports).
/// How long to give PnP to bind the driver + the driver to stamp the section before warning.
const ATTACH_GRACE: Duration = Duration::from_secs(3);
/// Per-pad driver-attach watcher: feed it the section's `driver_proto` on every service tick; it
/// logs the attach (INFO), a version mismatch (WARN), or — after [`ATTACH_GRACE`] of silence — one
/// diagnosis WARN combining the driver-store check and the devnode problem code. States never
/// repeat their log line, so the pump can call this at full rate.
pub(super) struct DriverAttach {
/// Driver label for log lines (`pf_xusb` / `pf_dualsense` / `pf_dualshock4`).
driver: &'static str,
/// The INF the driver store must hold for this driver (`pf_xusb.inf` / `pf_dualsense.inf`).
inf: &'static str,
/// The driver's own debug log, referenced in the diagnosis line.
driver_log: &'static str,
/// Bootstrap-mailbox name, for log lines (the DATA section is unnamed).
shm_name: String,
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
instance_id: Option<String>,
created: Instant,
state: AttachState,
}
enum AttachState {
Waiting,
/// Diagnosis logged; still watching so a late attach gets its INFO line.
Warned,
Attached,
}
impl DriverAttach {
pub(super) fn new(
driver: &'static str,
inf: &'static str,
driver_log: &'static str,
shm_name: String,
instance_id: Option<String>,
) -> DriverAttach {
DriverAttach {
driver,
inf,
driver_log,
shm_name,
instance_id,
created: Instant::now(),
state: AttachState::Waiting,
}
}
/// `driver_proto` is the section field the driver stamps once attached (0 = not attached).
pub(super) fn observe(&mut self, driver_proto: u32) {
match self.state {
AttachState::Attached => {}
AttachState::Waiting | AttachState::Warned if driver_proto != 0 => {
let late = matches!(self.state, AttachState::Warned);
tracing::info!(
driver = self.driver,
shm = %self.shm_name,
proto = driver_proto,
late,
"gamepad driver attached to the shared section"
);
if driver_proto != pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION {
tracing::warn!(
driver = self.driver,
driver_proto,
host_proto = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION,
"gamepad driver/host protocol mismatch — update the drivers: punktfunk-host.exe driver install --gamepad"
);
}
self.state = AttachState::Attached;
}
AttachState::Waiting if self.created.elapsed() >= ATTACH_GRACE => {
self.diagnose();
self.state = AttachState::Warned;
}
_ => {}
}
}
/// One-shot WARN with everything the host can find out about WHY the driver isn't attached:
/// driver-store presence, the devnode's PnP status/problem code, and where to look next.
fn diagnose(&self) {
let store = match driver_store_has(self.inf) {
Some(true) => "driver package present in the driver store",
Some(false) => {
"driver package NOT in the driver store — run: punktfunk-host.exe driver install --gamepad"
}
None => "driver store could not be queried (pnputil failed)",
};
let devnode = match &self.instance_id {
Some(id) => devnode_status_line(id),
None => {
"no per-session devnode (SwDeviceCreate failed earlier — see the warning above)"
.to_string()
}
};
tracing::warn!(
driver = self.driver,
shm = %self.shm_name,
grace_secs = ATTACH_GRACE.as_secs(),
store,
devnode = %devnode,
driver_log = self.driver_log,
"gamepad driver has not attached to the shared section — the virtual pad exists but no \
driver is serving it (games will not see it); an old (pre-sealed-channel) driver also \
reads as not-attached: update with punktfunk-host.exe driver install --gamepad \
(driver_log is only written by debug driver builds, or with the PFXUSB_DEBUG_LOG / \
PFDS_DEBUG_LOG system env var set + the device restarted)"
);
}
}
/// Driver-store inventory (`pnputil /enum-drivers`), lower-cased, fetched once per process — only
/// consulted on the failure path, so the one-off subprocess cost never hits a healthy session.
fn driver_store_inventory() -> &'static str {
static INV: OnceLock<String> = OnceLock::new();
INV.get_or_init(|| {
// Resolve pnputil by full System32 path — the host runs as SYSTEM and must not trust PATH /
// the CreateProcess search (which checks the launching EXE's own dir first), or a planted
// `pnputil.exe` beside the host binary would run elevated (security-review 2026-07-17).
let pnputil = std::env::var("SystemRoot")
.map(|r| format!(r"{r}\System32\pnputil.exe"))
.unwrap_or_else(|_| "pnputil.exe".to_string());
std::process::Command::new(&pnputil)
.arg("/enum-drivers")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase())
.unwrap_or_default()
})
}
/// Whether the driver store holds `inf` (e.g. `pf_xusb.inf`). `None` = pnputil unavailable/failed.
fn driver_store_has(inf: &str) -> Option<bool> {
let inv = driver_store_inventory();
if inv.is_empty() {
return None;
}
Some(inv.contains(&inf.to_ascii_lowercase()))
}
/// Human-readable PnP status of a devnode: driver bound/started or the CM problem code with a hint.
fn devnode_status_line(instance_id: &str) -> String {
let wide: Vec<u16> = instance_id
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut devinst = 0u32;
// SAFETY: `wide` is a valid NUL-terminated UTF-16 instance id; `devinst` receives the handle.
let cr = unsafe {
CM_Locate_DevNodeW(
&mut devinst,
PCWSTR(wide.as_ptr()),
CM_LOCATE_DEVNODE_NORMAL,
)
};
if cr != CR_SUCCESS {
return format!(
"devnode {instance_id} not found (CM_Locate_DevNodeW CR={})",
cr.0
);
}
let mut status = CM_DEVNODE_STATUS_FLAGS(0);
let mut problem = CM_PROB(0);
// SAFETY: devinst is the devnode located above; the two out-params receive status + problem.
let cr = unsafe { CM_Get_DevNode_Status(&mut status, &mut problem, devinst, 0) };
if cr != CR_SUCCESS {
return format!("devnode {instance_id}: status query failed (CR={})", cr.0);
}
if status.0 & DN_HAS_PROBLEM.0 != 0 {
return format!(
"devnode {instance_id} has PnP problem code {} ({}) [status 0x{:08x}]",
problem.0,
cm_problem_hint(problem.0),
status.0
);
}
format!(
"devnode {instance_id} status 0x{:08x} (driver_loaded={} started={})",
status.0,
status.0 & DN_DRIVER_LOADED.0 != 0,
status.0 & DN_STARTED.0 != 0,
)
}
/// The CM_PROB_* codes a virtual-pad devnode realistically hits, with the operator-facing cause.
fn cm_problem_hint(problem: u32) -> &'static str {
match problem {
1 => "not configured — no driver bound; install the drivers",
10 => "device failed to start — driver bound but its start failed; check the driver log",
18 => "reinstall required — re-run driver install",
24 => "device not present/working — PnP could not start the virtual devnode",
28 => "drivers not installed — the pf driver package is missing from the store or its certificate is not trusted",
31 => "driver failed to load — binding found the package but loading it failed",
39 => "driver corrupt or missing — reinstall the drivers",
43 => "reported failure after start — check the driver log",
52 => "driver signature rejected — certificate not in Root/TrustedPublisher, or blocked by Memory Integrity",
_ => "see Device Manager for this code",
}
}
@@ -0,0 +1,365 @@
//! Windows virtual Xbox 360 gamepad via the punktfunk **XUSB companion** UMDF driver
//! (`packaging/windows/drivers/pf-xusb`) — the in-tree replacement for ViGEmBus. One virtual Xbox 360
//! controller per client pad index, visible to classic **XInput** (`XInputGetState`) with no kernel
//! bus driver: each pad `SwDeviceCreate`s a `pf_xusb_<index>` devnode (the driver loads on it and
//! registers `GUID_DEVINTERFACE_XUSB`) and the host pushes the XInput state into an **unnamed** shared
//! DATA section the driver reaches over the **sealed channel** ([`PadChannel`] — handle duplicated
//! into its WUDFHost, bootstrapped via `Global\pfxusb-boot-<index>`; see
//! `design/gamepad-channel-sealing.md`). GameStream/Moonlight already speak the XInput conventions
//! (low-16 button bits, sticks 32768..32767 +Y up, triggers 0..255), so the state copy is ~1:1.
//!
//! Rumble flows back the other way: a game writes force-feedback via `XInputSetState`, the driver
//! parses the `SET_STATE` packet into the shared section, and [`GamepadManager::pump_rumble`] relays
//! level changes to the client (the universal 0xCA plane), mirroring the Linux `EV_FF` read path.
use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx};
use crate::pad_slots::PadSlots;
use anyhow::{anyhow, Result};
use punktfunk_core::input::{GamepadEvent, MAX_PADS};
use std::ffi::c_void;
use std::sync::atomic::{fence, AtomicU32, Ordering};
use std::time::{Duration, Instant};
use windows::core::{w, GUID, PCWSTR};
use windows::Win32::Devices::Enumeration::Pnp::{
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
};
use windows::Win32::Foundation::{CloseHandle, E_FAIL, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject};
// Shared-section layout — the single source of truth is `pf_driver_proto::gamepad::XusbShm` (offset
// asserts pin every field; the `pf_xusb` driver maps the same struct). Derive the size/offsets/magic from
// it so a layout change is a compile error, not a hand-synced literal (audit §6.1).
use pf_driver_proto::gamepad::XusbShm;
const SHM_SIZE: usize = core::mem::size_of::<XusbShm>();
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::XUSB_MAGIC; // "PFXU"
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: usize = core::mem::offset_of!(XusbShm, rumble_large); // large @28, small @29
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
/// Spawn the `pf_xusb_<index>` companion devnode (hardware id `pf_xusb`, enumerator `punktfunk`). The
/// INF (System class) binds our UMDF driver, which registers the XUSB interface. Unlike the HID pads,
/// no USB compatible-ids are needed — XInput finds the device by the interface GUID, not VID/PID — but
/// we still pass a deterministic non-null `pContainerId` (the null-sentinel trips an `xinput1_4`
/// slot-skip bug). `SwDeviceClose` removes it on drop.
fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
let hwids: Vec<u16> = "pf_xusb".encode_utf16().chain([0u16, 0u16]).collect();
let instid: Vec<u16> = format!("pf_xusb_{index}")
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let desc: Vec<u16> = "punktfunk Virtual Xbox 360 (XUSB)"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
// The pad index, stamped into the device Location — the driver reads it to poll `pfxusb-boot-<index>`
// (multi-pad). The buffer must outlive the SwDeviceCreate call (it does; we wait on the event).
let loc: Vec<u16> = format!("{index}")
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let container = GUID::from_values(0x5046_5855, 0x0000, 0x0000, [0, 0, 0, 0, 0, 0, 0, index]);
// SAFETY: zeroed then the fields we use are set; the buffers + container outlive the call.
let mut info: SW_DEVICE_CREATE_INFO = unsafe { std::mem::zeroed() };
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.pContainerId = &container;
info.pszDeviceDescription = PCWSTR(desc.as_ptr());
info.pszDeviceLocation = PCWSTR(loc.as_ptr());
info.CapabilityFlags = 0x0000_000B; // DriverRequired | SilentInstall | Removable
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
// `result` starts as E_FAIL, NOT S_OK: if the wait below times out, a zero-initialised HRESULT
// would read as success and mask the failure (found by the 2026-07 driver-health audit).
let mut ctx = SwCreateCtx {
event,
result: E_FAIL,
instance_id: [0; 128],
};
// SAFETY: info + buffers + ctx outlive the call (we wait on the event before returning).
let hsw = match unsafe {
SwDeviceCreate(
w!("punktfunk"),
w!("HTREE\\ROOT\\0"),
&info,
None,
Some(sw_create_cb),
Some(&mut ctx as *mut SwCreateCtx as *const c_void),
)
} {
Ok(h) => h,
Err(e) => {
// SAFETY: event is valid.
unsafe {
let _ = CloseHandle(event);
}
return Err(anyhow!("SwDeviceCreate(pf_xusb) failed: {e}"));
}
};
// SAFETY: event valid; block until PnP finishes enumerating, then check the callback result.
let wait = unsafe { WaitForSingleObject(event, 10_000) };
// SAFETY: event is valid.
unsafe {
let _ = CloseHandle(event);
}
if wait != WAIT_OBJECT_0 {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate(pf_xusb) enumeration callback never fired (10s) — PnP may be wedged"
));
}
if ctx.result.is_err() {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate(pf_xusb) enumeration failed: {:?}",
ctx.result
));
}
Ok((hsw, ctx.instance_id()))
}
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the sealed shared-memory channel.
struct XusbWinPad {
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
_sw: Option<super::gamepad_raii::SwDevice>,
/// The sealed channel: the unnamed DATA section (the `XusbShm`) + the bootstrap mailbox + the
/// handle-delivery state machine (drop closes both sections).
channel: PadChannel,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
packet: u32,
last_rumble_seq: u32,
}
impl XusbWinPad {
/// Create the sealed channel (unnamed DATA section + `Global\pfxusb-boot-<index>` mailbox), stamp
/// the pad index then the magic LAST, spawn the devnode, and eagerly deliver the DATA handle once
/// the driver publishes its pid.
fn open(index: u8) -> Result<XusbWinPad> {
let boot_name = pf_driver_proto::gamepad::xusb_boot_name(index);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
// The section arrives zeroed; stamp the pad index (the driver validates it against its own
// devnode index on attach) then the magic LAST (the driver only accepts it once magic is set).
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX is in range.
unsafe {
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
// Propagate a devnode-create failure instead of swallowing it: a swallowed failure left the
// pad with no devnode yet still reported success, so PadSlots latched a phantom pad (never
// re-created for the session's life) and the host logged a misleading "virtual Xbox 360
// created". Returning Err routes it through PadSlots' ERROR + capped-backoff retry — parity
// with the Linux uinput path, which self-heals for exactly this reason.
let (hsw, instance_id) = create_swdevice(index)?;
let _sw = Some(super::gamepad_raii::SwDevice::new(hsw));
// Bounded eager delivery: the driver's EvtDeviceAdd publishes its pid right away; handing it
// the DATA handle before we return means the pad is live for the game's first XInput poll.
// On a missing/old driver this waits out the window once and the service pump takes over.
channel.deliver_eager(Duration::from_millis(1500));
Ok(XusbWinPad {
_sw,
channel,
attach: super::gamepad_raii::DriverAttach::new(
"pf_xusb",
"pf_xusb.inf",
"C:\\Users\\Public\\pfxusb-driver.log",
boot_name,
instance_id,
),
packet: 0,
last_rumble_seq: 0,
})
}
/// Publish the XInput state to the section and bump the packet number (XInput uses it to detect
/// change). `buttons` is the XINPUT_GAMEPAD_* bitmap; sticks are i16, triggers u8.
#[allow(clippy::too_many_arguments)]
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
self.packet = self.packet.wrapping_add(1);
let base = self.channel.data_base();
// SAFETY: `base` is the start of the mapped section (`SHM_SIZE` bytes, owned by `Shm`); every
// `OFF_*` is a fixed in-range offset into it and `write_unaligned` handles the unaligned field
// writes. Single owner (`&mut self`), so no concurrent writer races these stores. `packet` (the
// field XInput reads to detect a new state) is published LAST: the `Release` fence orders the
// state-body stores above before the `Release` `AtomicU32` store of `packet`, so the driver —
// which `Acquire`-loads `packet` — never observes a bumped packet over a torn body on a
// weakly-ordered core (ARM64). On x86-TSO both are plain stores. `OFF_PACKET` (== 4) is
// 4-aligned off the page-aligned section base, so the `AtomicU32` view is valid (mirrors the
// seq-fenced publish in `gamepad_raii::PadChannel::create`).
unsafe {
std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons);
*base.add(OFF_LT) = lt;
*base.add(OFF_RT) = rt;
std::ptr::write_unaligned(base.add(OFF_LX) as *mut i16, lx);
std::ptr::write_unaligned(base.add(OFF_LY) as *mut i16, ly);
std::ptr::write_unaligned(base.add(OFF_RX) as *mut i16, rx);
std::ptr::write_unaligned(base.add(OFF_RY) as *mut i16, ry);
fence(Ordering::Release);
(*(base.add(OFF_PACKET) as *const AtomicU32)).store(self.packet, Ordering::Release);
}
}
/// Poll the section for a game's rumble (the driver bumps `rumble_seq` on each SET_STATE). Returns
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also ticks the sealed-channel
/// delivery (a late-binding driver gets its handle here) and feeds the driver-attach health
/// watcher (the driver stamps `driver_proto` once it maps the delivered section + per IOCTL).
fn service(&mut self) -> Option<(u8, u8)> {
self.channel.pump();
let base = self.channel.data_base();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes; `OFF_RUMBLE_SEQ` (== 24) is 4-aligned off the
// page-aligned base, so the `AtomicU32` view is valid. The driver bumps `rumble_seq` AFTER
// writing the rumble bytes, so an `Acquire` load here orders the `rumble_large`/`rumble_small`
// reads below after it — a fresh seq guarantees a coherent snapshot of the rumble bytes on a
// weakly-ordered core (ARM64). On x86-TSO it is a plain load.
let seq =
unsafe { (*(base.add(OFF_RUMBLE_SEQ) as *const AtomicU32)).load(Ordering::Acquire) };
if seq == self.last_rumble_seq {
return None;
}
self.last_rumble_seq = seq;
// SAFETY: rumble bytes at OFF_RUMBLE / OFF_RUMBLE+1.
let (large, small) = unsafe { (*base.add(OFF_RUMBLE), *base.add(OFF_RUMBLE + 1)) };
Some((large, small))
}
}
/// All virtual Xbox 360 pads of a session — the Windows analogue of the Linux uinput-xpad manager,
/// now backed by the XUSB companion driver. Same method surface (`new`/`handle`/`pump_rumble`) the
/// session input thread already drives.
/// How long a non-zero rumble may stay latched with the game NOT driving the pad (no `SET_STATE`)
/// before it is forced off. XInput vibration is level-triggered — it persists until the game sets
/// it to zero — so a game that latches a rumble and then stops calling `XInputSetState` (a residual
/// left at a menu / loading screen, or a plain forgotten stop) would otherwise drone to the client
/// forever (measured: a stuck `(0,512)` resent every 500 ms for 5.5 minutes). A real controller
/// stops when the app stops driving it; this mirrors that. It is keyed on game ACTIVITY (any
/// `SET_STATE`, even an unchanged one), so a rumble the game keeps asserting is never cut — only an
/// abandoned residual is. Kept above SDL's ~2 s internal rumble resend so an SDL-driven host game
/// (which re-issues the same level every ~2 s) refreshes the activity clock before this fires.
const RUMBLE_IDLE_TIMEOUT: Duration = Duration::from_millis(2500);
pub struct GamepadManager {
slots: PadSlots<XusbWinPad>,
last_rumble: Vec<(u8, u8)>,
/// When the game last drove each pad (bumped `rumble_seq` via `SET_STATE`). A non-zero
/// `last_rumble` older than [`RUMBLE_IDLE_TIMEOUT`] against this is a stale residual — see the
/// const's docs.
last_active: Vec<Instant>,
}
impl Default for GamepadManager {
fn default() -> GamepadManager {
GamepadManager::new()
}
}
impl GamepadManager {
pub fn new() -> GamepadManager {
GamepadManager {
slots: PadSlots::new(
"Xbox 360/Windows",
"Xbox 360",
" (install/repair: punktfunk-host.exe driver install --gamepad)",
),
last_rumble: vec![(0, 0); MAX_PADS],
last_active: (0..MAX_PADS).map(|_| Instant::now()).collect(),
}
}
fn ensure(&mut self, idx: usize) {
if self.slots.ensure(idx, XusbWinPad::open) {
tracing::info!(
index = idx,
"virtual Xbox 360 created (Windows XUSB companion)"
);
self.last_rumble[idx] = (0, 0);
self.last_active[idx] = Instant::now();
}
}
pub fn handle(&mut self, ev: &GamepadEvent) {
match ev {
GamepadEvent::Arrival { index, kind, .. } => {
tracing::info!(index, kind, "controller arrival (Xbox 360/Windows)");
self.ensure(*index as usize);
}
GamepadEvent::State(f) => {
let idx = f.index as usize;
if idx >= MAX_PADS {
return;
}
// Unplugs: drop any allocated pad whose mask bit cleared.
let swept = self.slots.sweep(f.active_mask);
for i in 0..MAX_PADS {
if swept & (1 << i) != 0 {
self.last_rumble[i] = (0, 0);
self.last_active[i] = Instant::now();
}
}
if f.active_mask & (1 << idx) == 0 {
return;
}
self.ensure(idx);
if let Some(pad) = self.slots.get_mut(idx) {
pad.write_state(
(f.buttons & 0xffff) as u16,
f.left_trigger,
f.right_trigger,
f.ls_x,
f.ls_y,
f.rs_x,
f.rs_y,
);
}
}
}
}
/// Relay any changed rumble level to the client. XUSB motors are 0..255; the wire carries
/// 0..65535, so scale by 257. `large` (low-frequency) → the datagram's `low`, `small`
/// (high-frequency) → `high` — matching the other backends.
pub fn pump_rumble(&mut self, mut send: impl FnMut(u16, u16, u16)) {
for (i, pad) in self.slots.iter_mut() {
if let Some((large, small)) = pad.service() {
// The game drove the pad this poll (SET_STATE bumped the seq) — refresh the
// activity clock even when the level is unchanged, so a rumble it keeps asserting
// never trips the stale-residual timeout below.
self.last_active[i] = Instant::now();
if self.last_rumble[i] != (large, small) {
self.last_rumble[i] = (large, small);
send(i as u16, large as u16 * 257, small as u16 * 257);
}
} else if self.last_rumble[i] != (0, 0)
&& self.last_active[i].elapsed() >= RUMBLE_IDLE_TIMEOUT
{
// A non-zero rumble is latched but the game has not driven the pad for
// RUMBLE_IDLE_TIMEOUT — a residual it forgot to stop. Force it off (and forward
// the zero) so the resend loop stops droning it to the client. See the const docs.
tracing::info!(
index = i,
prev_low = self.last_rumble[i].0 as u16 * 257,
prev_high = self.last_rumble[i].1 as u16 * 257,
"rumble: stale residual (game stopped driving the pad) — forcing off"
);
self.last_rumble[i] = (0, 0);
send(i as u16, 0, 0);
}
}
}
}
@@ -0,0 +1,355 @@
//! Resident virtual HID mouse on Windows via the UMDF minidriver (`packaging/windows/drivers/pf-mouse`).
//!
//! **Why**: with no pointing device attached (a headless streaming box — no dongle), win32k reports
//! the cursor as absent (`GetSystemMetrics(SM_MOUSEPRESENT)` = 0) and DWM never composites a cursor
//! into the pf-vdisplay frame — the streamed desktop has an invisible pointer even though
//! `SendInput` moves it. Keeping ONE virtual HID mouse devnode alive for the host's lifetime makes
//! Windows always consider a pointer present and draw the cursor — the Sunshine/Parsec-class fix,
//! with zero client changes. Injection stays [`super::sendinput`]; the report path here is
//! exercised by `punktfunk-host vmouse-spike` (on-glass validation) and is the future
//! higher-fidelity injection route.
//!
//! Transport is the **sealed pad channel** verbatim ([`PadChannel`],
//! `design/gamepad-channel-sealing.md`): an unnamed 64-B `MouseShm` DATA section the host
//! duplicates into the driver's WUDFHost, bootstrapped via the named `Global\pfmouse-boot-0`
//! mailbox. The devnode is `SwDeviceCreate`'d like a pad but held for the PROCESS lifetime (the
//! [`ensure_resident`] thread never drops it), so the pointer survives across sessions; it
//! disappears with the host service, which is exactly when nobody is streaming.
use super::dualsense_windows::{create_swdevice, SwDeviceProfile};
use super::gamepad_raii::{DriverAttach, PadChannel};
use anyhow::Result;
use pf_driver_proto::mouse::{input_report, mouse_boot_name, MouseShm, MOUSE_MAGIC};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Condvar, Mutex};
use std::time::Duration;
use windows::Win32::Foundation::POINT;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
const SHM_SIZE: usize = core::mem::size_of::<MouseShm>();
const OFF_IN_SEQ: usize = core::mem::offset_of!(MouseShm, in_seq);
const OFF_REPORT: usize = core::mem::offset_of!(MouseShm, report);
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(MouseShm, driver_proto);
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(MouseShm, driver_heartbeat);
const OFF_PAD_INDEX: usize = core::mem::offset_of!(MouseShm, pad_index);
/// The one resident virtual mouse: the `SwDeviceCreate`'d `pf_mouse_0` devnode (the pf-mouse HID
/// minidriver loads on it → Windows counts a pointer present) plus the sealed shared-memory
/// channel. Dropping it removes the devnode — [`ensure_resident`] therefore never drops it.
pub struct VirtualMouse {
/// Devnode RAII (`SwDeviceClose` on drop). `None` falls back to an out-of-band devnode.
_sw: Option<super::gamepad_raii::SwDevice>,
channel: PadChannel,
attach: DriverAttach,
seq: u32,
}
impl VirtualMouse {
/// Create the sealed channel (unnamed DATA section + `Global\pfmouse-boot-0` mailbox), stamp
/// the index + the magic LAST, then spawn the devnode and eagerly deliver the DATA handle.
pub fn open() -> Result<VirtualMouse> {
let boot_name = mouse_boot_name(0);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range. Index
// first, magic LAST — the same publish order the pads use.
unsafe {
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, 0u32);
std::ptr::write_unaligned(base as *mut u32, MOUSE_MAGIC);
}
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: "pf_mouse_0",
container_tag: 0x5046_4D4F, // "PFMO" — never grouped with a pad's container
container_index: 0,
hwid: "pf_mouse",
// An obviously-virtual identity (PF:MO). The synthesized USB bus tokens are inert for
// a mouse (nothing fingerprints them); reusing the shared profile keeps one code path.
usb_vid_pid: "VID_5046&PID_4D4F",
usb_mi: None,
description: "punktfunk Virtual Mouse",
}) {
Ok((h, i)) => (Some(h), i),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_mouse devnode");
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
channel.deliver_eager(Duration::from_millis(1500));
Ok(VirtualMouse {
_sw,
channel,
attach: DriverAttach::new(
"pf_mouse",
"pf_mouse.inf",
"C:\\Users\\Public\\pfmouse-driver.log",
boot_name,
instance_id,
),
seq: 0,
})
}
/// Publish an input report (5-bit buttons, absolute 15-bit x/y, wheel/pan deltas) and bump
/// `in_seq` (Release) — the driver's timer completes a pended `READ_REPORT` with it. Unused by
/// sessions today (`SendInput` injects); the spike drives it, and a future fidelity mode will.
pub fn send_report(&mut self, buttons: u8, x: u16, y: u16, wheel: i8, pan: i8) {
let r = input_report(buttons, x, y, wheel, pan);
self.seq = self.seq.wrapping_add(1).max(1); // never publish seq 0 (= "nothing yet")
let base = self.channel.data_base();
// SAFETY: base points at SHM_SIZE bytes; the report slot is OFF_REPORT..+8 and OFF_IN_SEQ
// (== 4) is 4-aligned off the page-aligned base, so the AtomicU32 view is valid. The report
// bytes are published BEFORE the seq (Release) — the driver's Acquire load of `in_seq`
// therefore observes the matching report.
unsafe {
std::ptr::copy_nonoverlapping(r.as_ptr(), base.add(OFF_REPORT), r.len());
(*(base.add(OFF_IN_SEQ) as *const AtomicU32)).store(self.seq, Ordering::Release);
}
}
/// One service tick: pump the sealed-channel delivery and feed the driver-attach health
/// watcher (the driver's 8 ms timer stamps `driver_proto` while it has the section mapped).
pub fn service(&mut self) {
self.channel.pump();
self.attach.observe(self.driver_proto());
}
fn driver_proto(&self) -> u32 {
// SAFETY: base points at SHM_SIZE bytes; OFF_DRIVER_PROTO is in range.
unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
}
}
fn driver_heartbeat(&self) -> u32 {
// SAFETY: base points at SHM_SIZE bytes; OFF_DRIVER_HEARTBEAT is in range.
unsafe {
std::ptr::read_unaligned(
self.channel.data_base().add(OFF_DRIVER_HEARTBEAT) as *const u32
)
}
}
}
/// One pending compose-kick aim, desktop coordinates: the target display's rect plus the
/// virtual-desktop bounds to normalize against (both from CCD, so they describe the CONSOLE's
/// layout whatever session this process is in). Newest-wins single slot — kicks are idempotent
/// damage nudges, queueing them would only multiply pointer blips.
struct KickAim {
rect: (i32, i32, i32, i32),
bounds: (i32, i32, i32, i32),
}
struct KickSlot {
slot: Mutex<Option<KickAim>>,
wake: Condvar,
}
static KICK: KickSlot = KickSlot {
slot: Mutex::new(None),
wake: Condvar::new(),
};
/// True while the keeper's mouse is open AND the pf-mouse driver is attached (its 8 ms timer
/// stamps `driver_proto`) — the only state in which a kick's reports actually reach win32k.
static MOUSE_READY: AtomicBool = AtomicBool::new(false);
/// Request a pointer jiggle on the given display through the resident virtual mouse — the
/// COMPOSE KICK's reliable arm. A report from a HID device is REAL input to win32k: it wakes a
/// powered-off display subsystem (lid-closed / display idle-off / modern standby), resets idle
/// timers, counts as user presence, and is delivered regardless of the calling process's session
/// or the active desktop — every condition under which the `SendInput` kick is silently impotent
/// (wrong session → wrong input queue; secure desktop → blocked; display-off → nothing to
/// damage). Asynchronous: the keeper thread (which owns the one process-wide mouse) executes it
/// within its tick. Returns `false` when the resident mouse isn't up (opted out, driver not
/// installed, not yet attached) — the caller falls back to `SendInput`.
pub(crate) fn hid_kick(rect: (i32, i32, i32, i32), bounds: (i32, i32, i32, i32)) -> bool {
if !MOUSE_READY.load(Ordering::Relaxed) {
return false;
}
*KICK.slot.lock().unwrap() = Some(KickAim { rect, bounds });
KICK.wake.notify_one();
true
}
/// Execute one compose kick on the keeper thread: park the pointer at the target rect's center,
/// dwell one composition interval, wiggle ~2 px, then put it back where it was. Every report is
/// device-level input (see [`hid_kick`]). The dwell is load-bearing (the Stage-W3 on-glass
/// finding, same as the SendInput jump path): DWM samples the cursor position at the next vsync
/// tick, so a sub-tick round trip composes nothing. The gaps also respect the driver's 8 ms
/// report timer — back-to-back writes into the single report slot would coalesce.
///
/// The restore is best-effort via `GetCursorPos`: in a wrong-session host it describes the wrong
/// session's pointer, so the console pointer is instead left near the target's center — which is
/// the streamed display, exactly where the pointer is about to be useful.
fn perform_kick(m: &mut VirtualMouse, aim: KickAim) {
let (bx, by, bw, bh) = aim.bounds;
if bw <= 0 || bh <= 0 {
return;
}
// Field-log which kick arm fired (the SendInput arm logs in kick_dwm_compose) — a lid-closed
// repro should show this line followed by the driver's first acquired frame.
tracing::debug!(
rect = ?aim.rect,
bounds = ?aim.bounds,
"HID compose kick — parking the pointer on the target display (display wake + damage)"
);
let map = |px: i32, py: i32| -> (u16, u16) {
let nx = ((px - bx).clamp(0, bw - 1) as i64 * 0x7FFF) / i64::from(bw - 1).max(1);
let ny = ((py - by).clamp(0, bh - 1) as i64 * 0x7FFF) / i64::from(bh - 1).max(1);
(nx as u16, ny as u16)
};
let mut p = POINT::default();
// SAFETY: plain FFI; `p` is a valid out-param for this synchronous call.
let orig = unsafe { GetCursorPos(&mut p) }
.is_ok()
.then_some((p.x, p.y));
let (rx, ry, rw, rh) = aim.rect;
let (cx, cy) = map(rx + rw / 2, ry + rh / 2);
// ~2 desktop pixels in HID units, at least 1 — the wiggle must actually move the pointer.
let dx = ((2 * 0x7FFF) / bw.max(1)).max(1) as u16;
m.send_report(0, cx, cy, 0, 0);
std::thread::sleep(Duration::from_millis(35));
m.send_report(0, cx.saturating_add(dx).min(0x7FFF), cy, 0, 0);
std::thread::sleep(Duration::from_millis(35));
match orig {
Some((ox, oy)) => {
let (ox, oy) = map(ox, oy);
m.send_report(0, ox, oy, 0, 0);
}
None => m.send_report(0, cx, cy, 0, 0),
}
}
/// Make sure the resident virtual mouse exists (idempotent, best-effort). Called whenever an
/// [`InjectorService`](crate::InjectorService) starts — multiple services (native +
/// GameStream) share the ONE process-wide mouse, guarded here. Spawns a keeper thread that owns
/// the devnode for the process lifetime and pumps the channel at a slow tick (delivery is eager at
/// open; the pump only handles a late WUDFHost + feeds the attach diagnostics).
///
/// `PUNKTFUNK_NO_VIRTUAL_MOUSE=1` opts out (diagnostics, or an operator who objects to a virtual
/// pointer device).
pub(crate) fn ensure_resident() {
use std::sync::OnceLock;
static STARTED: OnceLock<()> = OnceLock::new();
STARTED.get_or_init(|| {
if std::env::var_os("PUNKTFUNK_NO_VIRTUAL_MOUSE").is_some_and(|v| v != "0") {
tracing::info!(
"virtual HID mouse disabled (PUNKTFUNK_NO_VIRTUAL_MOUSE) — with no physical \
pointer attached, Windows will not draw a cursor into the stream"
);
return;
}
// Hand the capture crate its HID compose-kick hook (the one-way-edge inversion: pf-capture
// never reaches back into inject). Registered exactly when the resident mouse is being
// brought up; until the driver actually attaches, `hid_kick` reports not-ready and the
// kick falls back to SendInput.
let _ = pf_capture::HID_COMPOSE_KICK.set(hid_kick);
if let Err(e) = std::thread::Builder::new()
.name("punktfunk-vmouse".into())
.spawn(keeper_thread)
{
tracing::warn!(error = %e, "virtual-mouse keeper thread spawn failed");
}
});
}
/// Open-with-retry, then hold + pump forever. Open only realistically fails on a mailbox squat
/// (another punktfunk-host instance) — retry slowly; a missing/failed DRIVER is not an open
/// failure (the devnode exists but nothing binds), which [`DriverAttach`] diagnoses via the pump.
/// Each tick also publishes kick-readiness ([`MOUSE_READY`]) and executes at most one pending
/// compose kick ([`hid_kick`]) — the condvar wait keeps kick latency at "immediately", not "next
/// 250 ms tick", while an idle keeper still only wakes 4×/s.
fn keeper_thread() {
loop {
match VirtualMouse::open() {
Ok(mut m) => {
tracing::info!(
"resident virtual HID mouse created (pf_mouse — keeps SM_MOUSEPRESENT true \
so DWM composites the cursor on headless hosts)"
);
loop {
m.service();
MOUSE_READY.store(m.driver_proto() != 0, Ordering::Relaxed);
let (mut slot, _timeout) = KICK
.wake
.wait_timeout_while(
KICK.slot.lock().unwrap(),
Duration::from_millis(250),
|k| k.is_none(),
)
.unwrap();
let aim = slot.take();
drop(slot);
if let Some(aim) = aim {
if m.driver_proto() != 0 {
perform_kick(&mut m, aim);
}
}
}
}
Err(e) => {
tracing::warn!(
error = %format!("{e:#}"),
"virtual HID mouse open failed — retrying in 60s (headless hosts stream an \
invisible cursor until it exists)"
);
std::thread::sleep(Duration::from_secs(60));
}
}
}
}
/// `vmouse-spike` (dev validation): hold the virtual mouse and drive the REAL cursor through the
/// HID report path — proves the full chain (SwDeviceCreate → INF bind → mshidumdf → mouhid →
/// win32k) on-glass. Run with the host service STOPPED (the resident mouse owns the mailbox name
/// otherwise). Verify while it holds: `Get-PnpDevice` shows the pf_mouse devnode + a HID child,
/// `GetSystemMetrics(SM_MOUSEPRESENT)` = 1 with no physical mouse, and the cursor sweeps a
/// horizontal line mid-screen.
pub fn spike_hold(secs: u64) -> Result<()> {
let mut m = VirtualMouse::open()?;
println!("virtual HID mouse devnode up (5046:4D4F) — waiting for the driver to attach…");
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while m.driver_proto() == 0 && std::time::Instant::now() < deadline {
m.service();
std::thread::sleep(Duration::from_millis(50));
}
if m.driver_proto() == 0 {
println!(
"driver never attached (10s). Install it: punktfunk-host.exe driver install --gamepad \
--dir <stage> (pf_mouse.inf ships with the gamepad drivers); see the WARN above."
);
} else {
println!(
"driver attached (proto {}). Sweeping the cursor for {secs}s — watch the glass: the \
pointer should glide left↔right across mid-screen; wheel ticks every second.",
m.driver_proto()
);
}
let t0 = std::time::Instant::now();
let mut i: u64 = 0;
let beat_before = m.driver_heartbeat();
while t0.elapsed() < Duration::from_secs(secs) {
// Triangle-wave X sweep over the middle 3/4 of the axis, fixed mid-screen Y; one wheel
// tick per second so scroll delivery is visible too.
let phase = (i % 240) as i32; // 240 steps × 16 ms ≈ 4 s per round trip
let tri = if phase < 120 { phase } else { 240 - phase };
let x = 4096 + (tri as u32 * (24576 / 120)) as u16;
let wheel: i8 = if i % 60 == 0 { 1 } else { 0 };
m.send_report(0, x, 0x4000, wheel, 0);
m.service();
i += 1;
std::thread::sleep(Duration::from_millis(16));
}
let beat = m.driver_heartbeat();
println!(
"vmouse-spike: done (driver heartbeat advanced {} ticks — {}). Devnode removed on exit.",
beat.wrapping_sub(beat_before),
if beat != beat_before {
"driver alive"
} else {
"driver NOT ticking"
}
);
Ok(())
}
@@ -0,0 +1,461 @@
//! Windows input injection via `SendInput` (Win32 KeyboardAndMouse) — the Windows analogue of
//! [`super::wlr`]: absolute mouse normalized to the virtual desktop, relative mouse for games,
//! scancode keyboard, scroll, buttons. Survives UAC/lock desktop switches with Sunshine's
//! retry-on-failure model: the thread stays bound to its desktop and only reattaches
//! (`OpenInputDesktop`/`SetThreadDesktop`) when `SendInput` reports a short write (the input
//! desktop switched) — no per-event reattach overhead.
//!
//! **Keyboard conventions** (see [`crate::KEY_FLAG_SEMANTIC_VK`]): first-party punktfunk
//! clients send **US-positional** VKs (the physical key's US-layout VK — layout-independent by
//! construction, the mirror of the Linux host's `vk_to_evdev`), resolved here through the fixed
//! [`positional_vk_to_scan`] table. GameStream/Moonlight clients send **layout-semantic** VKs
//! (Sunshine's model), resolved under the foreground app's layout. Never resolve a positional VK
//! through a layout: this thread runs in the SYSTEM service, whose layout is unrelated to the
//! user's, and any layout re-reads a *position* as a *character* — on a German host that is
//! exactly the y↔z swap / ü-on-ö scramble.
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it.
#![deny(clippy::undocumented_unsafe_blocks)]
use anyhow::Result;
use punktfunk_core::input::{InputEvent, InputKind};
use std::mem::size_of;
use windows::Win32::System::StationsAndDesktops::{
CloseDesktop, OpenInputDesktop, SetThreadDesktop, DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS,
HDESK,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
GetKeyboardLayout, MapVirtualKeyExW, SendInput, HKL, INPUT, INPUT_0, INPUT_KEYBOARD,
INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MAPVK_VK_TO_VSC_EX, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE,
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL,
MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT, VIRTUAL_KEY,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetSystemMetrics, GetWindowThreadProcessId, SM_CXVIRTUALSCREEN,
SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};
use super::InputInjector;
const ABS_MAX: f64 = 65535.0; // SendInput absolute coords are 0..65535 over the chosen surface.
const GENERIC_ALL: u32 = 0x1000_0000;
const XBUTTON1: u32 = 0x0001;
const XBUTTON2: u32 = 0x0002;
pub struct SendInputInjector {
desktop: Option<HDESK>,
}
// SAFETY: `SendInputInjector` holds only an `Option<HDESK>` (a desktop handle). The host creates
// and drives it from a single dedicated injector thread; the handle is opened, rebound, and closed
// on whichever thread owns the value, and the type is not `Sync`, so there is never concurrent
// access. A desktop `HDESK` is not thread-affine for ownership (`CloseDesktop` works from any
// thread; `SetThreadDesktop` rebinds the current thread), so transferring ownership via `Send` is
// sound.
unsafe impl Send for SendInputInjector {}
impl SendInputInjector {
pub fn open() -> Result<Self> {
let mut me = Self { desktop: None };
me.reattach_input_desktop(); // best-effort
tracing::info!("SendInput injector ready (Win32 KeyboardAndMouse)");
Ok(me)
}
/// Bind this thread to the desktop currently receiving input. UAC / lock screen / Ctrl-Alt-Del
/// swap the input desktop; `SendInput` silently no-ops unless our thread is on it.
fn reattach_input_desktop(&mut self) {
// SAFETY: `OpenInputDesktop`/`SetThreadDesktop`/`CloseDesktop` are FFI calls passed only
// by-value args (constant desktop flags, a `bool`, an access mask). `OpenInputDesktop`
// yields an owned `HDESK` only on `Ok`; we then either install it with `SetThreadDesktop`
// (closing the previously-owned handle exactly once) or close the fresh handle on failure —
// so every handle is closed exactly once and none is used after close. `SetThreadDesktop`
// only rebinds this calling thread, which is where the injector runs.
unsafe {
match OpenInputDesktop(
DESKTOP_CONTROL_FLAGS(0),
false,
DESKTOP_ACCESS_FLAGS(GENERIC_ALL),
) {
Ok(h) => {
if SetThreadDesktop(h).is_ok() {
if let Some(old) = self.desktop.replace(h) {
let _ = CloseDesktop(old);
}
} else {
let _ = CloseDesktop(h);
}
}
Err(_) => { /* not privileged enough for the secure desktop; stay put */ }
}
}
}
/// Inject with Sunshine's retry-on-failure model: the thread stays bound to whatever desktop it
/// last attached to (no per-event `OpenInputDesktop`/`SetThreadDesktop` — two syscalls saved on
/// every mouse move), and only when `SendInput` reports a short write (0 = the input desktop
/// switched out from under us, e.g. into UAC/lock) do we reattach to the now-current input desktop
/// and retry once. This serves both the normal and secure desktops with no steady-state overhead.
fn send(&mut self, inputs: &[INPUT]) -> Result<()> {
// SAFETY: `inputs` is a live `&[INPUT]` slice that outlives this synchronous `SendInput`
// call; `size_of::<INPUT>()` is the exact per-element stride Win32 requires as `cbSize`. The
// call only reads the array (one event per element) and returns the count injected.
let n = unsafe { SendInput(inputs, size_of::<INPUT>() as i32) };
if n as usize == inputs.len() {
return Ok(());
}
// Short write → the input desktop likely changed. Reattach + retry once.
self.reattach_input_desktop();
// SAFETY: same as the first `SendInput` — `inputs` is the identical live slice outliving the
// call and `cbSize == size_of::<INPUT>()`; only re-issued after reattaching the input desktop.
let n = unsafe { SendInput(inputs, size_of::<INPUT>() as i32) };
if n as usize != inputs.len() {
anyhow::bail!(
"SendInput injected {n}/{} events (blocked desktop?)",
inputs.len()
);
}
Ok(())
}
}
impl Drop for SendInputInjector {
fn drop(&mut self) {
if let Some(h) = self.desktop.take() {
// SAFETY: `h` is the `HDESK` this injector owned (moved out of `self.desktop`);
// `CloseDesktop` runs once here in `Drop` on that still-valid handle, with no later use —
// no double close.
unsafe {
let _ = CloseDesktop(h);
}
}
}
}
impl InputInjector for SendInputInjector {
fn inject(&mut self, event: &InputEvent) -> Result<()> {
// No per-event desktop reattach — `send` reattaches lazily only on a short write (desktop
// switch). The injector is bound to the input desktop at open() and follows switches on demand.
match event.kind {
InputKind::MouseMove => {
let mi = MOUSEINPUT {
dx: event.x,
dy: event.y,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseMoveAbs => {
let w = (event.flags >> 16) & 0xffff;
let h = event.flags & 0xffff;
if w == 0 || h == 0 {
return Ok(()); // contract: drop zero extent
}
let (_vx, _vy, vw, vh) = virtual_desktop_rect();
// One virtual output spanning the virtual desktop: map client (0..w,0..h) -> 0..65535.
let cx = (event.x.clamp(0, w as i32)) as f64 / w as f64;
let cy = (event.y.clamp(0, h as i32)) as f64 / h as f64;
let ax = (cx * ABS_MAX).round() as i32;
let ay = (cy * ABS_MAX).round() as i32;
let _ = (vw, vh); // virtual-desktop rect reserved for multi-output mapping
let mi = MOUSEINPUT {
dx: ax,
dy: ay,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseButtonDown | InputKind::MouseButtonUp => {
let down = event.kind == InputKind::MouseButtonDown;
let (flag, data) = match event.code {
1 => (
if down {
MOUSEEVENTF_LEFTDOWN
} else {
MOUSEEVENTF_LEFTUP
},
0u32,
),
2 => (
if down {
MOUSEEVENTF_MIDDLEDOWN
} else {
MOUSEEVENTF_MIDDLEUP
},
0,
),
3 => (
if down {
MOUSEEVENTF_RIGHTDOWN
} else {
MOUSEEVENTF_RIGHTUP
},
0,
),
4 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON1,
),
5 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON2,
),
_ => return Ok(()),
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: data,
dwFlags: flag,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseScroll => {
// GameStream WHEEL_DELTA(120) units. Windows WHEEL positive=up (matches GameStream —
// no flip, unlike Wayland); HWHEEL positive=right (matches). x is 120-scaled already.
let horizontal = event.code == 1;
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: event.x as u32, // signed wheel delta reinterpreted as DWORD
dwFlags: if horizontal {
MOUSEEVENTF_HWHEEL
} else {
MOUSEEVENTF_WHEEL
},
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::KeyDown | InputKind::KeyUp => {
let down = event.kind == InputKind::KeyDown;
let vk = (event.code & 0xff) as u16;
let semantic = (event.flags & crate::KEY_FLAG_SEMANTIC_VK) != 0;
// Positional wire VKs (first-party clients) resolve through the fixed US table —
// never through a layout (module docs). The table covers only the layout-VARIANT
// typing area; everything else (F-row, nav, numpad, modifiers) is layout-invariant
// and falls through to `MapVirtualKeyExW` (same result under any layout, and it
// keeps the proven extended-bit handling). Semantic VKs (Moonlight) skip the table
// and resolve under the FOREGROUND app's layout — the layout the receiving app
// will decode our scancode with (Sunshine's model; the service thread's own layout
// is not the user's).
let table = if semantic {
None
} else {
positional_vk_to_scan(vk)
};
let (scan, extended) = match table {
Some(scan) => (scan, forced_extended(vk)), // typing area: never E0-extended
None => {
let hkl = if semantic { foreground_hkl() } else { None };
// SAFETY: `MapVirtualKeyExW` is a pure value translation (VK → scancode);
// all three args are by-value (`u32`, the `MAPVK_VK_TO_VSC_EX` map-type
// constant, an optional `HKL` handle used only as a lookup key). It
// dereferences no pointer and returns a `u32` — FFI-`unsafe` only.
let sc_ex = unsafe { MapVirtualKeyExW(vk as u32, MAPVK_VK_TO_VSC_EX, hkl) };
if sc_ex == 0 {
return Ok(()); // unmappable -> drop
}
(
(sc_ex & 0xff) as u16,
(sc_ex & 0xe000) == 0xe000 || forced_extended(vk),
)
}
};
let mut flags = KEYEVENTF_SCANCODE;
if extended {
flags |= KEYEVENTF_EXTENDEDKEY;
}
if !down {
flags |= KEYEVENTF_KEYUP;
}
let ki = KEYBDINPUT {
wVk: VIRTUAL_KEY(0),
wScan: scan,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
};
self.send(&[key(ki)])
}
// Gamepad goes through the XUSB backend. Touch: no SendInput equivalent -> no-op.
InputKind::GamepadButton
| InputKind::GamepadAxis
| InputKind::GamepadState
| InputKind::GamepadRemove
| InputKind::GamepadArrival
| InputKind::TouchDown
| InputKind::TouchMove
| InputKind::TouchUp => Ok(()),
}
}
}
fn mouse(mi: MOUSEINPUT) -> INPUT {
INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 { mi },
}
}
fn key(ki: KEYBDINPUT) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 { ki },
}
}
fn virtual_desktop_rect() -> (i32, i32, i32, i32) {
// SAFETY: each `GetSystemMetrics` takes a single by-value `SYSTEM_METRICS_INDEX` constant and
// returns an `i32`; it dereferences no pointer and has no side effects — FFI-`unsafe` only.
unsafe {
(
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN),
)
}
}
// VKs Windows wants flagged extended even when the scancode high bits aren't set: the editing
// cluster (Ins/Del/Home/End/PgUp/PgDn = 0x21..0x28, 0x2D, 0x2E), the Win keys (0x5B/0x5C/0x5D),
// RCtrl (0xA3), RAlt (0xA5), Pause (0x90). MAPVK_VK_TO_VSC_EX already encodes E0 for most; this is a
// thin safety net.
fn forced_extended(vk: u16) -> bool {
matches!(
vk,
0x21..=0x28 | 0x2D | 0x2E | 0x5B | 0x5C | 0x5D | 0xA3 | 0xA5 | 0x90
)
}
/// US-positional VK → set-1 make scancode for the layout-**variant** typing area (letters, the
/// digit row, OEM punctuation, the ISO 102nd key). The exact mirror of the Linux host's
/// `crate::vk_to_evdev` — for these keys the evdev code IS the set-1 scancode — and of
/// every first-party client's capture table, so the positional round trip is
/// identity-by-construction. Layout-invariant keys are deliberately absent (the
/// `MapVirtualKeyExW` fallback resolves them identically under any layout, with its proven
/// extended-key handling). All listed keys are plain make codes — never E0-extended.
fn positional_vk_to_scan(vk: u16) -> Option<u16> {
Some(match vk {
0x30 => 0x0B, // VK_0
0x31..=0x39 => vk - 0x31 + 0x02, // VK_1..VK_9 → 0x02..0x0A
0x41 => 0x1E, // A
0x42 => 0x30, // B
0x43 => 0x2E, // C
0x44 => 0x20, // D
0x45 => 0x12, // E
0x46 => 0x21, // F
0x47 => 0x22, // G
0x48 => 0x23, // H
0x49 => 0x17, // I
0x4A => 0x24, // J
0x4B => 0x25, // K
0x4C => 0x26, // L
0x4D => 0x32, // M
0x4E => 0x31, // N
0x4F => 0x18, // O
0x50 => 0x19, // P
0x51 => 0x10, // Q
0x52 => 0x13, // R
0x53 => 0x1F, // S
0x54 => 0x14, // T
0x55 => 0x16, // U
0x56 => 0x2F, // V
0x57 => 0x11, // W
0x58 => 0x2D, // X
0x59 => 0x15, // Y (US position — a QWERTZ host renders it as Z)
0x5A => 0x2C, // Z (US position)
0xBA => 0x27, // VK_OEM_1 ;: (DE: ö)
0xBB => 0x0D, // VK_OEM_PLUS =+
0xBC => 0x33, // VK_OEM_COMMA ,<
0xBD => 0x0C, // VK_OEM_MINUS -_ (DE: ß)
0xBE => 0x34, // VK_OEM_PERIOD .>
0xBF => 0x35, // VK_OEM_2 /?
0xC0 => 0x29, // VK_OEM_3 `~ (DE: ^)
0xDB => 0x1A, // VK_OEM_4 [{ (DE: ü)
0xDC => 0x2B, // VK_OEM_5 \|
0xDD => 0x1B, // VK_OEM_6 ]}
0xDE => 0x28, // VK_OEM_7 '" (DE: ä)
0xE2 => 0x56, // VK_OEM_102 <>| (ISO key next to left shift)
_ => return None,
})
}
/// The keyboard layout of the thread owning the foreground window — the layout the app receiving
/// our injected scancodes will decode them under (Sunshine's model for semantic Moonlight VKs).
/// `None` when there is no foreground window (secure desktop, transient) — the caller then falls
/// back to the current thread's layout, today's behavior.
fn foreground_hkl() -> Option<HKL> {
// SAFETY: three read-only queries. `GetForegroundWindow` takes nothing and returns a possibly
// null `HWND` (checked). `GetWindowThreadProcessId` reads the window's owning thread id (the
// process-id out-param is `None`, allowed). `GetKeyboardLayout` maps a thread id to its input
// locale by value. No pointer we own is dereferenced; a stale/foreign `tid` yields a null HKL,
// which is filtered.
unsafe {
let hwnd = GetForegroundWindow();
if hwnd.is_invalid() {
return None;
}
let tid = GetWindowThreadProcessId(hwnd, None);
let hkl = GetKeyboardLayout(tid);
(!hkl.is_invalid()).then_some(hkl)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The positional table must mirror the Linux host's `vk_to_evdev` exactly — for the typing
/// area the evdev code IS the set-1 scancode, so any divergence would make the same wire VK
/// land on different physical keys on the two hosts.
#[test]
fn positional_table_mirrors_linux_vk_to_evdev() {
let mut checked = 0;
for vk in 0x01..=0xFEu16 {
if let Some(scan) = positional_vk_to_scan(vk) {
assert_eq!(
Some(scan),
crate::vk_to_evdev(vk as u8),
"vk 0x{vk:02X}: sendinput scancode diverges from vk_to_evdev"
);
checked += 1;
}
}
assert_eq!(checked, 48, "typing-area coverage changed unexpectedly");
}
/// The German-scramble regression pins: the US-position VKs the first-party clients send for
/// the physical Y/Z/ö/ü keys must resolve to those physical positions, not through a layout.
#[test]
fn positional_pins_for_the_qwertz_scramble() {
assert_eq!(positional_vk_to_scan(0x59), Some(0x15)); // VK_Y → US-Y position (QWERTZ: Z key)
assert_eq!(positional_vk_to_scan(0x5A), Some(0x2C)); // VK_Z → US-Z position (QWERTZ: Y key)
assert_eq!(positional_vk_to_scan(0xBA), Some(0x27)); // VK_OEM_1 → ;: position (QWERTZ: ö)
assert_eq!(positional_vk_to_scan(0xDB), Some(0x1A)); // VK_OEM_4 → [{ position (QWERTZ: ü)
// Layout-invariant keys stay out of the table (resolved via MapVirtualKeyExW).
assert_eq!(positional_vk_to_scan(0x70), None); // VK_F1
assert_eq!(positional_vk_to_scan(0x0D), None); // VK_RETURN
assert_eq!(positional_vk_to_scan(0xA0), None); // VK_LSHIFT
}
}
@@ -0,0 +1,233 @@
//! Virtual Steam Deck controller on Windows via the UMDF minidriver — the Windows analogue of
//! the Linux UHID Deck ([`super::steam_controller`]'s `SteamProto`), sharing its whole codec
//! ([`super::steam_proto`]: the byte-exact `ID_CONTROLLER_DECK_STATE` serializer, the
//! `XInput`/rich mappers, the `0xEB` rumble parser).
//!
//! Transport = the sealed shared-memory channel + a `SwDeviceCreate` devnode (device-type 3),
//! like the PS pads — with the promotion lever the N4 spike proved: the synthesized USB
//! hardware ids carry **`&MI_02`** (the Deck's wired controller interface), which hidclass
//! mirrors into the HID child and hidapi/Steam parse as `bInterfaceNumber`. Steam Input then
//! claims the pad exactly like a physical wired Deck (`!! Steam controller device opened`,
//! XInput slot reserved — observed live on `.173`), so games get native Deck glyphs +
//! trackpads + gyro + back grips through Steam's own remapping.
//!
//! Feedback: Steam drives Deck rumble (`0xEB`) and trackpad haptic pulses (`0x8F`) via
//! SET_FEATURE on the unnumbered report; the driver republishes those into the section's
//! output slot (report-id-0 prefixed), where [`parse_steam_output`] reads the exact wire shape
//! the Linux path sees. No gamepad-mode entry pulse here — that gate lives in the Linux
//! kernel's evdev parser; Steam-on-Windows reads the raw reports directly.
use super::dualsense_windows::{
create_swdevice, SwDeviceProfile, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT, OFF_OUTPUT,
OFF_OUT_SEQ, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE,
};
use super::gamepad_raii::PadChannel;
use super::steam_proto::{
neutral_deck_report, parse_steam_output, serialize_deck_state, SteamState, STEAM_REPORT_LEN,
};
use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::Result;
use punktfunk_core::quic::RichInput;
use std::time::Duration;
/// A single virtual Steam Deck: the `SwDeviceCreate`'d `pf_deck_<index>` devnode plus the sealed
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait).
pub struct DeckWinPad {
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
_sw: Option<super::gamepad_raii::SwDevice>,
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
channel: PadChannel,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
seq: u32,
last_out_seq: u32,
}
impl DeckWinPad {
/// Create the sealed channel, stamp `device_type = Steam Deck` FIRST + the pad index + the
/// neutral Deck frame + the magic LAST, then spawn the `pf_deck_<index>` devnode with the
/// `MI_02` USB identity Steam's promotion gate requires.
fn open(index: u8) -> Result<DeckWinPad> {
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
unsafe {
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_STEAMDECK;
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
std::ptr::write_unaligned(
base.add(OFF_INPUT) as *mut [u8; STEAM_REPORT_LEN],
neutral_deck_report(),
);
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let inst = format!("pf_deck_{index}");
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_steamdeck",
usb_vid_pid: "VID_28DE&PID_1205",
// The wired Deck controller interface — WITHOUT this the HID child carries no MI_
// token, hidapi reports interface 0, and Steam never claims the pad (the N4
// spike's run-1 failure).
usb_mi: Some(2),
description: "punktfunk Virtual Steam Deck",
}) {
Ok((h, i)) => (Some(h), i),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; Steam Deck devnode unavailable");
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
// Bounded eager delivery — the driver must read `device_type = 3` before hidclass asks
// it for descriptors, or the pad would enumerate with the default DualSense identity.
channel.deliver_eager(Duration::from_millis(1500));
Ok(DeckWinPad {
_sw,
channel,
attach: super::gamepad_raii::DriverAttach::new(
"pf_steamdeck",
"pf_dualsense.inf", // one driver package serves every identity
"C:\\Users\\Public\\pfds-driver.log",
boot_name,
instance_id,
),
seq: 0,
last_out_seq: 0,
})
}
/// Serialize `st` into the Deck state frame and publish it to the section's input slot.
fn write_state(&mut self, st: &SteamState) {
self.seq = self.seq.wrapping_add(1);
let mut r = [0u8; STEAM_REPORT_LEN];
serialize_deck_state(&mut r, st, self.seq);
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
unsafe {
std::ptr::copy_nonoverlapping(
r.as_ptr(),
self.channel.data_base().add(OFF_INPUT),
r.len(),
)
};
}
/// Poll the section's output slot; parse a newly-published Steam command (`0xEB` rumble /
/// `0x8F` haptic pulse — republished by the driver off SET_FEATURE) into feedback. Also
/// ticks the sealed-channel delivery and the driver-attach health watcher.
fn service(&mut self) -> Option<(u16, u16)> {
self.channel.pump();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
};
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes.
let seq = unsafe {
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
};
if seq == self.last_out_seq {
return None;
}
self.last_out_seq = seq;
let mut out = [0u8; 64];
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
unsafe {
std::ptr::copy_nonoverlapping(
self.channel.data_base().add(OFF_OUTPUT),
out.as_mut_ptr(),
64,
)
};
parse_steam_output(&out).rumble
}
}
/// The Windows-Deck half of the shared stateful manager (see [`PadProto`]): the sealed-channel
/// open under the promoted Deck identity, the same [`SteamState`] mappers as the Linux backend,
/// and the section feedback poll. Lifecycle (slot table, unplug sweep, heartbeat, rumble dedup)
/// lives in [`UhidManager`].
#[derive(Default)]
pub struct DeckWinProto;
impl PadProto for DeckWinProto {
type Pad = DeckWinPad;
type State = SteamState;
const LABEL: &'static str = "Steam Deck/Windows";
const DEVICE: &'static str = "Steam Deck";
const CREATE_HINT: &'static str =
" (install/repair: punktfunk-host.exe driver install --gamepad)";
fn open(&mut self, idx: u8) -> Result<DeckWinPad> {
let p = DeckWinPad::open(idx)?;
tracing::info!(
index = idx,
"virtual Steam Deck created (Windows UMDF shm channel, MI_02 promoted identity)"
);
Ok(p)
}
fn neutral(&self) -> SteamState {
SteamState::neutral()
}
/// Merge buttons/sticks/triggers, preserving the rich-plane fields (trackpads + motion +
/// pad clicks arrive separately and must survive a button-only frame) — identical to the
/// Linux `SteamProto::merge_frame`.
fn merge_frame(
&self,
prev: &SteamState,
f: &punktfunk_core::input::GamepadFrame,
) -> SteamState {
use super::steam_proto::btn;
let mut s = SteamState::from_gamepad(
f.buttons,
f.ls_x,
f.ls_y,
f.rs_x,
f.rs_y,
f.left_trigger,
f.right_trigger,
);
s.rpad_x = prev.rpad_x;
s.rpad_y = prev.rpad_y;
s.lpad_x = prev.lpad_x;
s.lpad_y = prev.lpad_y;
s.gyro = prev.gyro;
s.accel = prev.accel;
s.buttons |= prev.buttons & (btn::RPAD_TOUCH | btn::LPAD_TOUCH);
s.lpad_click = prev.lpad_click;
s.rpad_click = prev.rpad_click;
s
}
fn apply_rich(&self, st: &mut SteamState, rich: RichInput) {
st.apply_rich(rich);
}
fn write_state(&self, pad: &mut DeckWinPad, st: &SteamState) {
pad.write_state(st);
}
/// Poll the section for Steam's feedback: motor rumble on the universal 0xCA plane. The
/// Deck has no rich host→client feedback plane (no lightbar / adaptive triggers), so
/// `hidout` stays empty — parity with the Linux backend.
fn service(&self, pad: &mut DeckWinPad, _idx: u8) -> PadFeedback {
// The Deck poll returns `Some` exactly when a fresh output report landed (a seq bump), so
// its presence is the game-activity signal, even when the rumble level is unchanged.
let rumble = pad.service();
PadFeedback {
rumble,
hidout: Vec::new(),
game_drove: Some(rumble.is_some()),
}
}
}
/// All virtual Steam Deck pads of a Windows session — the analogue of the Linux
/// `SteamControllerManager`, with the same method surface (via the shared [`UhidManager`]) as
/// the other Windows pad managers.
pub type SteamDeckWindowsManager = UhidManager<DeckWinProto>;