refactor(windows-host): confine platform code under windows/ + linux/ folders (Goal-1 stage 6)
Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
capture/{windows,linux}/ encode/{windows,linux}/ inject/{windows,linux,proto}/
audio/{windows,linux}/ vdisplay/{windows,linux}/
src/windows/ (service, wgc_helper, win_adapter, win_display)
src/linux/ (dmabuf_fence, drm_sync, zerocopy/)
Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.
Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
//! Virtual Sony DualSense on Windows via the UMDF minidriver (`packaging/windows/dualsense-driver`).
|
||||
//!
|
||||
//! 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 a **named shared-memory section**
|
||||
//! `Global\pfds-shm-<idx>` (256 B: magic `u32@0`, input report `@8`, output seq `u32@72`, output
|
||||
//! report `@76`). The host creates the section (privileged → a permissive SDDL so the WUDFHost can
|
||||
//! open it); the driver maps it from its timer, 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 crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::ffi::c_void;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
|
||||
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::{CreateEventW, SetEvent, WaitForSingleObject};
|
||||
|
||||
/// Shared-section layout — the single source of truth is [`pf_vdisplay_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_vdisplay_proto::gamepad::PadShm>();
|
||||
pub(super) const SHM_MAGIC: u32 = pf_vdisplay_proto::gamepad::PAD_MAGIC; // "PFDS"
|
||||
pub(super) const OFF_INPUT: usize = core::mem::offset_of!(pf_vdisplay_proto::gamepad::PadShm, input);
|
||||
pub(super) const OFF_OUT_SEQ: usize =
|
||||
core::mem::offset_of!(pf_vdisplay_proto::gamepad::PadShm, out_seq);
|
||||
pub(super) const OFF_OUTPUT: usize = core::mem::offset_of!(pf_vdisplay_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_vdisplay_proto::gamepad::PadShm, device_type);
|
||||
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_vdisplay_proto::gamepad::DEVTYPE_DUALSHOCK4;
|
||||
|
||||
/// 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 shared-memory section the driver maps.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||
struct DsWinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds. `None` falls back to an out-of-band
|
||||
/// `pf_dualsense` devnode (installer/devgen).
|
||||
hsw: Option<HSWDEVICE>,
|
||||
map: HANDLE,
|
||||
view: *mut u8,
|
||||
seq: u8,
|
||||
ts: u32,
|
||||
last_out_seq: u32,
|
||||
}
|
||||
|
||||
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
|
||||
#[repr(C)]
|
||||
struct SwCreateCtx {
|
||||
event: HANDLE,
|
||||
result: HRESULT,
|
||||
}
|
||||
|
||||
/// `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`).
|
||||
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.
|
||||
unsafe {
|
||||
let c = ctx as *mut SwCreateCtx;
|
||||
(*c).result = result;
|
||||
let _ = SetEvent((*c).event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Index for the deterministic per-pad ContainerId.
|
||||
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,
|
||||
/// 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 `docs/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> {
|
||||
// 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 usb_rev = format!("USB\\{}&REV_0100", p.usb_vid_pid);
|
||||
let usb = format!("USB\\{}", 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 map `pfds-shm-<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 per-pad ContainerId {50464453-0000-0000-0000-0000000000<idx>} ("PFDS").
|
||||
let container = GUID::from_values(
|
||||
0x5046_4453,
|
||||
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())? };
|
||||
let mut ctx = SwCreateCtx {
|
||||
event,
|
||||
result: HRESULT(0),
|
||||
};
|
||||
// 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.
|
||||
unsafe {
|
||||
WaitForSingleObject(event, 10_000);
|
||||
let _ = CloseHandle(event);
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// Create + map the named section `Global\pfds-shm-<index>`, zeroed, with a permissive DACL so the
|
||||
/// WUDFHost (whatever account it runs as) can open it. Returns `(section handle, mapped base)`; the
|
||||
/// caller stamps the device-type + initial input report and finally the magic. Shared by both Windows
|
||||
/// pad backends (DualSense + DualShock 4).
|
||||
pub(super) fn create_shm_section(index: u8) -> Result<(HANDLE, *mut u8)> {
|
||||
let name = HSTRING::from(pf_vdisplay_proto::gamepad::pad_shm_name(index));
|
||||
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: the SDDL literal is valid; psd receives an allocated descriptor (freed by the OS when
|
||||
// the process exits — acceptable for a host-lifetime object).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;GA;;;WD)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
|
||||
// SAFETY: anonymous (pagefile-backed) section of SHM_SIZE bytes with the SDDL above.
|
||||
let map = unsafe {
|
||||
CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
SHM_SIZE as u32,
|
||||
PCWSTR(name.as_ptr()),
|
||||
)?
|
||||
};
|
||||
// SAFETY: map is a valid section handle; map the whole thing.
|
||||
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, SHM_SIZE) };
|
||||
if view.Value.is_null() {
|
||||
// SAFETY: map is valid.
|
||||
unsafe {
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(anyhow!("MapViewOfFile failed for {name}"));
|
||||
}
|
||||
let base = view.Value as *mut u8;
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
unsafe { std::ptr::write_bytes(base, 0, SHM_SIZE) };
|
||||
Ok((map, base))
|
||||
}
|
||||
|
||||
impl DsWinPad {
|
||||
/// Create + map the section `Global\pfds-shm-<index>`, stamp the magic, then spawn the
|
||||
/// `root\pf_dualsense` devnode (the driver loads on it and maps the section). The devnode lives
|
||||
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
fn open(index: u8) -> Result<DsWinPad> {
|
||||
let (map, base) = create_shm_section(index)?;
|
||||
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
|
||||
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
unsafe {
|
||||
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 `pf_dualsense`
|
||||
// devnode (installer / dev-box devgen).
|
||||
let inst = format!("pf_pad_{index}");
|
||||
let hsw = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
container_index: index,
|
||||
hwid: "pf_dualsense",
|
||||
usb_vid_pid: "VID_054C&PID_0CE6",
|
||||
description: "punktfunk Virtual DualSense",
|
||||
}) {
|
||||
Ok(h) => Some(h),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_dualsense devnode");
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(DsWinPad {
|
||||
hsw,
|
||||
map,
|
||||
view: base,
|
||||
seq: 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.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: view points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe { std::ptr::copy_nonoverlapping(r.as_ptr(), self.view.add(OFF_INPUT), r.len()) };
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||
let mut fb = DsFeedback::default();
|
||||
// SAFETY: view points at SHM_SIZE bytes.
|
||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_OUT_SEQ) as *const u32) };
|
||||
if seq != self.last_out_seq {
|
||||
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.view.add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
};
|
||||
parse_ds_output(pad, &out, &mut fb);
|
||||
}
|
||||
fb
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DsWinPad {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: hsw (if any) owns the devnode; view/map from MapViewOfFile/CreateFileMappingW.
|
||||
unsafe {
|
||||
if let Some(h) = self.hsw {
|
||||
SwDeviceClose(h);
|
||||
}
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: self.view as *mut c_void,
|
||||
});
|
||||
let _ = CloseHandle(self.map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All virtual DualSense pads of a session — the Windows analogue of
|
||||
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface so the session input
|
||||
/// thread drives either backend identically.
|
||||
pub struct DualSenseWindowsManager {
|
||||
pads: Vec<Option<DsWinPad>>,
|
||||
state: Vec<DsState>,
|
||||
last_rumble: Vec<(u16, u16)>,
|
||||
last_write: Vec<Instant>,
|
||||
broken: bool,
|
||||
}
|
||||
|
||||
impl Default for DualSenseWindowsManager {
|
||||
fn default() -> DualSenseWindowsManager {
|
||||
DualSenseWindowsManager::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DualSenseWindowsManager {
|
||||
pub fn new() -> DualSenseWindowsManager {
|
||||
DualSenseWindowsManager {
|
||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
||||
state: vec![DsState::neutral(); MAX_PADS],
|
||||
last_rumble: vec![(0, 0); MAX_PADS],
|
||||
last_write: vec![Instant::now(); MAX_PADS],
|
||||
broken: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle one decoded controller event (create/destroy by mask, then merge button/stick state).
|
||||
pub fn handle(&mut self, ev: &GamepadEvent) {
|
||||
match ev {
|
||||
GamepadEvent::Arrival { index, kind, .. } => {
|
||||
tracing::info!(index, kind, "controller arrival (DualSense/Windows)");
|
||||
self.ensure(*index as usize);
|
||||
}
|
||||
GamepadEvent::State(f) => {
|
||||
let idx = f.index as usize;
|
||||
if idx >= MAX_PADS {
|
||||
return;
|
||||
}
|
||||
for (i, slot) in self.pads.iter_mut().enumerate() {
|
||||
if slot.is_some() && f.active_mask & (1 << i) == 0 {
|
||||
tracing::info!(index = i, "controller unplugged (DualSense/Windows)");
|
||||
*slot = None;
|
||||
self.state[i] = DsState::neutral();
|
||||
self.last_rumble[i] = (0, 0);
|
||||
}
|
||||
}
|
||||
if f.active_mask & (1 << idx) == 0 {
|
||||
return;
|
||||
}
|
||||
self.ensure(idx);
|
||||
let prev = self.state[idx];
|
||||
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.touch = prev.touch;
|
||||
s.gyro = prev.gyro;
|
||||
s.accel = prev.accel;
|
||||
self.state[idx] = s;
|
||||
self.write(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply one rich client→host event (touchpad contact / motion sample) to an existing pad.
|
||||
pub fn apply_rich(&mut self, rich: RichInput) {
|
||||
let idx = match rich {
|
||||
RichInput::Touchpad { pad, .. } | RichInput::Motion { pad, .. } => pad as usize,
|
||||
};
|
||||
if idx >= MAX_PADS || self.pads[idx].is_none() {
|
||||
return;
|
||||
}
|
||||
match rich {
|
||||
RichInput::Touchpad {
|
||||
finger,
|
||||
active,
|
||||
x,
|
||||
y,
|
||||
..
|
||||
} => {
|
||||
let slot = (finger as usize).min(1);
|
||||
let t = &mut self.state[idx].touch[slot];
|
||||
t.active = active;
|
||||
t.id = slot as u8;
|
||||
t.x = ((x as u32 * (DS_TOUCH_W - 1) as u32) / u16::MAX as u32) as u16;
|
||||
t.y = ((y as u32 * (DS_TOUCH_H - 1) as u32) / u16::MAX as u32) as u16;
|
||||
}
|
||||
RichInput::Motion { gyro, accel, .. } => {
|
||||
self.state[idx].gyro = gyro;
|
||||
self.state[idx].accel = accel;
|
||||
}
|
||||
}
|
||||
self.write(idx);
|
||||
}
|
||||
|
||||
fn write(&mut self, idx: usize) {
|
||||
let st = self.state[idx];
|
||||
if let Some(pad) = self.pads[idx].as_mut() {
|
||||
pad.write_state(&st);
|
||||
}
|
||||
self.last_write[idx] = Instant::now();
|
||||
}
|
||||
|
||||
/// Re-emit each live pad's current report if it's been silent for `max_gap` (the driver's timer
|
||||
/// streams whatever's in the section, so this just keeps the section fresh / future-proofs parity
|
||||
/// with the UHID backend's heartbeat).
|
||||
pub fn heartbeat(&mut self, max_gap: Duration) {
|
||||
let now = Instant::now();
|
||||
for i in 0..self.pads.len() {
|
||||
if self.pads[i].is_some() && now.duration_since(self.last_write[i]) >= max_gap {
|
||||
self.write(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure(&mut self, idx: usize) {
|
||||
if idx >= MAX_PADS || self.pads[idx].is_some() || self.broken {
|
||||
return;
|
||||
}
|
||||
match DsWinPad::open(idx as u8) {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualSense created (Windows UMDF shm channel)"
|
||||
);
|
||||
self.pads[idx] = Some(p);
|
||||
self.state[idx] = DsState::neutral();
|
||||
self.last_rumble[idx] = (0, 0);
|
||||
self.last_write[idx] = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %format!("{e:#}"), "virtual DualSense creation failed — controller input disabled");
|
||||
self.broken = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service every pad: poll the section for a game's feedback. `rumble` fires `(index, low, high)`
|
||||
/// only on change (universal 0xCA plane); `hidout` fires for each rich DualSense feedback event
|
||||
/// (lightbar / player LEDs / adaptive triggers — 0xCD plane).
|
||||
pub fn pump(
|
||||
&mut self,
|
||||
mut rumble: impl FnMut(u16, u16, u16),
|
||||
mut hidout: impl FnMut(HidOutput),
|
||||
) {
|
||||
for i in 0..self.pads.len() {
|
||||
let Some(pad) = self.pads[i].as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let fb = pad.service(i as u8);
|
||||
if let Some(r) = fb.rumble {
|
||||
if self.last_rumble[i] != r {
|
||||
self.last_rumble[i] = r;
|
||||
rumble(i as u16, r.0, r.1);
|
||||
}
|
||||
}
|
||||
for h in fb.hidout {
|
||||
hidout(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
//! 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
|
||||
//! `Global\pfds-shm-<idx>` shared section the driver maps), 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
|
||||
//! 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_shm_section, create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE,
|
||||
OFF_INPUT, OFF_OUTPUT, OFF_OUT_SEQ, SHM_MAGIC,
|
||||
};
|
||||
use super::dualshock4_proto::{
|
||||
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
||||
};
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::Result;
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::ffi::c_void;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::Memory::{UnmapViewOfFile, MEMORY_MAPPED_VIEW_ADDRESS};
|
||||
|
||||
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the mapped
|
||||
/// shared section. Dropping it removes the devnode and unmaps + closes the section.
|
||||
struct Ds4WinPad {
|
||||
hsw: Option<HSWDEVICE>,
|
||||
map: HANDLE,
|
||||
view: *mut u8,
|
||||
counter: u8,
|
||||
ts: u16,
|
||||
last_out_seq: u32,
|
||||
}
|
||||
|
||||
impl Ds4WinPad {
|
||||
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
|
||||
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
|
||||
fn open(index: u8) -> Result<Ds4WinPad> {
|
||||
let (map, base) = create_shm_section(index)?;
|
||||
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = DEVTYPE_DUALSHOCK4;
|
||||
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 = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
container_index: index,
|
||||
hwid: "pf_dualshock4",
|
||||
usb_vid_pid: "VID_054C&PID_09CC",
|
||||
description: "punktfunk Virtual DualShock 4",
|
||||
}) {
|
||||
Ok(h) => Some(h),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; DualShock 4 devnode unavailable");
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(Ds4WinPad {
|
||||
hsw,
|
||||
map,
|
||||
view: base,
|
||||
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: view points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe { std::ptr::copy_nonoverlapping(r.as_ptr(), self.view.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.
|
||||
fn service(&mut self) -> Ds4Feedback {
|
||||
let mut fb = Ds4Feedback::default();
|
||||
// SAFETY: view points at SHM_SIZE bytes.
|
||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_OUT_SEQ) as *const u32) };
|
||||
if seq != self.last_out_seq {
|
||||
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.view.add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||
};
|
||||
parse_ds4_output(&out, &mut fb);
|
||||
}
|
||||
fb
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ds4WinPad {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: hsw (if any) owns the devnode; view/map from MapViewOfFile/CreateFileMappingW.
|
||||
unsafe {
|
||||
if let Some(h) = self.hsw {
|
||||
SwDeviceClose(h);
|
||||
}
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: self.view as *mut c_void,
|
||||
});
|
||||
let _ = CloseHandle(self.map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All virtual DualShock 4 pads of a session — the Windows analogue of
|
||||
/// [`DualShock4Manager`](super::dualshock4::DualShock4Manager), with the same method surface as the
|
||||
/// Windows DualSense manager so the session input thread drives either backend identically.
|
||||
pub struct DualShock4WindowsManager {
|
||||
pads: Vec<Option<Ds4WinPad>>,
|
||||
state: Vec<DsState>,
|
||||
last_rumble: Vec<(u16, u16)>,
|
||||
last_led: Vec<Option<(u8, u8, u8)>>,
|
||||
last_write: Vec<Instant>,
|
||||
broken: bool,
|
||||
}
|
||||
|
||||
impl Default for DualShock4WindowsManager {
|
||||
fn default() -> DualShock4WindowsManager {
|
||||
DualShock4WindowsManager::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DualShock4WindowsManager {
|
||||
pub fn new() -> DualShock4WindowsManager {
|
||||
DualShock4WindowsManager {
|
||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
||||
state: vec![DsState::neutral(); MAX_PADS],
|
||||
last_rumble: vec![(0, 0); MAX_PADS],
|
||||
last_led: vec![None; MAX_PADS],
|
||||
last_write: vec![Instant::now(); MAX_PADS],
|
||||
broken: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle one decoded controller event (create/destroy by mask, then merge button/stick state).
|
||||
pub fn handle(&mut self, ev: &GamepadEvent) {
|
||||
match ev {
|
||||
GamepadEvent::Arrival { index, kind, .. } => {
|
||||
tracing::info!(index, kind, "controller arrival (DualShock 4/Windows)");
|
||||
self.ensure(*index as usize);
|
||||
}
|
||||
GamepadEvent::State(f) => {
|
||||
let idx = f.index as usize;
|
||||
if idx >= MAX_PADS {
|
||||
return;
|
||||
}
|
||||
for (i, slot) in self.pads.iter_mut().enumerate() {
|
||||
if slot.is_some() && f.active_mask & (1 << i) == 0 {
|
||||
tracing::info!(index = i, "controller unplugged (DualShock 4/Windows)");
|
||||
*slot = None;
|
||||
self.state[i] = DsState::neutral();
|
||||
self.last_rumble[i] = (0, 0);
|
||||
self.last_led[i] = None;
|
||||
}
|
||||
}
|
||||
if f.active_mask & (1 << idx) == 0 {
|
||||
return;
|
||||
}
|
||||
self.ensure(idx);
|
||||
let prev = self.state[idx];
|
||||
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.touch = prev.touch;
|
||||
s.gyro = prev.gyro;
|
||||
s.accel = prev.accel;
|
||||
self.state[idx] = s;
|
||||
self.write(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply one rich client→host event (touchpad contact / motion sample) to an existing pad.
|
||||
pub fn apply_rich(&mut self, rich: RichInput) {
|
||||
let idx = match rich {
|
||||
RichInput::Touchpad { pad, .. } | RichInput::Motion { pad, .. } => pad as usize,
|
||||
};
|
||||
if idx >= MAX_PADS || self.pads[idx].is_none() {
|
||||
return;
|
||||
}
|
||||
match rich {
|
||||
RichInput::Touchpad {
|
||||
finger,
|
||||
active,
|
||||
x,
|
||||
y,
|
||||
..
|
||||
} => {
|
||||
let slot = (finger as usize).min(1);
|
||||
let t = &mut self.state[idx].touch[slot];
|
||||
t.active = active;
|
||||
t.id = slot as u8;
|
||||
t.x = ((x as u32 * (DS4_TOUCH_W - 1) as u32) / u16::MAX as u32) as u16;
|
||||
t.y = ((y as u32 * (DS4_TOUCH_H - 1) as u32) / u16::MAX as u32) as u16;
|
||||
}
|
||||
RichInput::Motion { gyro, accel, .. } => {
|
||||
self.state[idx].gyro = gyro;
|
||||
self.state[idx].accel = accel;
|
||||
}
|
||||
}
|
||||
self.write(idx);
|
||||
}
|
||||
|
||||
fn write(&mut self, idx: usize) {
|
||||
let st = self.state[idx];
|
||||
if let Some(pad) = self.pads[idx].as_mut() {
|
||||
pad.write_state(&st);
|
||||
}
|
||||
self.last_write[idx] = Instant::now();
|
||||
}
|
||||
|
||||
/// Re-emit each live pad's current report if it's been silent for `max_gap` (parity with the
|
||||
/// other backends' heartbeat — keeps the section fresh).
|
||||
pub fn heartbeat(&mut self, max_gap: Duration) {
|
||||
let now = Instant::now();
|
||||
for i in 0..self.pads.len() {
|
||||
if self.pads[i].is_some() && now.duration_since(self.last_write[i]) >= max_gap {
|
||||
self.write(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure(&mut self, idx: usize) {
|
||||
if idx >= MAX_PADS || self.pads[idx].is_some() || self.broken {
|
||||
return;
|
||||
}
|
||||
match Ds4WinPad::open(idx as u8) {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualShock 4 created (Windows UMDF shm channel)"
|
||||
);
|
||||
self.pads[idx] = Some(p);
|
||||
self.state[idx] = DsState::neutral();
|
||||
self.last_rumble[idx] = (0, 0);
|
||||
self.last_led[idx] = None;
|
||||
self.last_write[idx] = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %format!("{e:#}"), "virtual DualShock 4 creation failed — controller input disabled");
|
||||
self.broken = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service every pad: poll the section for a game's feedback. `rumble` fires `(index, low, high)`
|
||||
/// only on change (universal 0xCA plane); `hidout` fires the lightbar (0xCD `Led`), deduped.
|
||||
pub fn pump(
|
||||
&mut self,
|
||||
mut rumble: impl FnMut(u16, u16, u16),
|
||||
mut hidout: impl FnMut(HidOutput),
|
||||
) {
|
||||
for i in 0..self.pads.len() {
|
||||
let Some(pad) = self.pads[i].as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let fb = pad.service();
|
||||
if let Some(r) = fb.rumble {
|
||||
if self.last_rumble[i] != r {
|
||||
self.last_rumble[i] = r;
|
||||
rumble(i as u16, r.0, r.1);
|
||||
}
|
||||
}
|
||||
if let Some(rgb) = fb.led {
|
||||
if self.last_led[i] != Some(rgb) {
|
||||
self.last_led[i] = Some(rgb);
|
||||
hidout(HidOutput::Led {
|
||||
pad: i as u8,
|
||||
r: rgb.0,
|
||||
g: rgb.1,
|
||||
b: rgb.2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
//! Windows virtual Xbox 360 gamepad via the punktfunk **XUSB companion** UMDF driver
|
||||
//! (`packaging/windows/xusb-driver`) — 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 the shared section
|
||||
//! `Global\pfxusb-shm-<index>`. 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.
|
||||
//!
|
||||
//! NB: the driver currently maps `Global\pfxusb-shm-0` (hardcoded), so a single pad (index 0) is
|
||||
//! fully correct; mixed multi-pad needs the driver to read its own index first (same limitation as
|
||||
//! the DualSense backend).
|
||||
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::ffi::c_void;
|
||||
use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||
};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
|
||||
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::{CreateEventW, SetEvent, WaitForSingleObject};
|
||||
|
||||
// Shared-section layout — the single source of truth is `pf_vdisplay_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_vdisplay_proto::gamepad::XusbShm;
|
||||
const SHM_SIZE: usize = core::mem::size_of::<XusbShm>();
|
||||
const SHM_MAGIC: u32 = pf_vdisplay_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
|
||||
|
||||
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
|
||||
#[repr(C)]
|
||||
struct SwCreateCtx {
|
||||
event: HANDLE,
|
||||
result: HRESULT,
|
||||
}
|
||||
|
||||
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result + wake the creator.
|
||||
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.
|
||||
unsafe {
|
||||
let c = ctx as *mut SwCreateCtx;
|
||||
(*c).result = result;
|
||||
let _ = SetEvent((*c).event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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 map `pfxusb-shm-<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())? };
|
||||
let mut ctx = SwCreateCtx {
|
||||
event,
|
||||
result: HRESULT(0),
|
||||
};
|
||||
// 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.
|
||||
unsafe {
|
||||
WaitForSingleObject(event, 10_000);
|
||||
let _ = CloseHandle(event);
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
|
||||
struct XusbWinPad {
|
||||
hsw: Option<HSWDEVICE>,
|
||||
map: HANDLE,
|
||||
view: *mut u8,
|
||||
packet: u32,
|
||||
last_rumble_seq: u32,
|
||||
}
|
||||
|
||||
impl XusbWinPad {
|
||||
/// Create + map `Global\pfxusb-shm-<index>`, stamp the magic, then spawn the devnode.
|
||||
fn open(index: u8) -> Result<XusbWinPad> {
|
||||
let name = HSTRING::from(pf_vdisplay_proto::gamepad::xusb_shm_name(index));
|
||||
|
||||
// Permissive DACL so the WUDFHost (whatever account) can open the section.
|
||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||
// SAFETY: SDDL literal valid; psd receives an OS-freed descriptor (host-lifetime — fine).
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;GA;;;WD)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut psd,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
let sa = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
// SAFETY: anonymous (pagefile-backed) section of SHM_SIZE bytes with the SDDL above.
|
||||
let map = unsafe {
|
||||
CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
Some(&sa),
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
SHM_SIZE as u32,
|
||||
PCWSTR(name.as_ptr()),
|
||||
)?
|
||||
};
|
||||
// SAFETY: map is a valid section handle; map the whole thing.
|
||||
let view = unsafe { MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, SHM_SIZE) };
|
||||
if view.Value.is_null() {
|
||||
// SAFETY: map is valid.
|
||||
unsafe {
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(anyhow!("MapViewOfFile failed for {name}"));
|
||||
}
|
||||
let base = view.Value as *mut u8;
|
||||
// Zero the section then stamp the magic LAST (the driver only accepts it once magic is set).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||
unsafe {
|
||||
std::ptr::write_bytes(base, 0, SHM_SIZE);
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
let hsw = match create_swdevice(index) {
|
||||
Ok(h) => Some(h),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; XUSB devnode unavailable");
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(XusbWinPad {
|
||||
hsw,
|
||||
map,
|
||||
view: base,
|
||||
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);
|
||||
// SAFETY: view points at SHM_SIZE bytes; all offsets are in range.
|
||||
unsafe {
|
||||
std::ptr::write_unaligned(self.view.add(OFF_BUTTONS) as *mut u16, buttons);
|
||||
*self.view.add(OFF_LT) = lt;
|
||||
*self.view.add(OFF_RT) = rt;
|
||||
std::ptr::write_unaligned(self.view.add(OFF_LX) as *mut i16, lx);
|
||||
std::ptr::write_unaligned(self.view.add(OFF_LY) as *mut i16, ly);
|
||||
std::ptr::write_unaligned(self.view.add(OFF_RX) as *mut i16, rx);
|
||||
std::ptr::write_unaligned(self.view.add(OFF_RY) as *mut i16, ry);
|
||||
std::ptr::write_unaligned(self.view.add(OFF_PACKET) as *mut u32, self.packet);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn service(&mut self) -> Option<(u8, u8)> {
|
||||
// SAFETY: view points at SHM_SIZE bytes.
|
||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_RUMBLE_SEQ) as *const u32) };
|
||||
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 { (*self.view.add(OFF_RUMBLE), *self.view.add(OFF_RUMBLE + 1)) };
|
||||
Some((large, small))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XusbWinPad {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: hsw (if any) owns the devnode; view/map from MapViewOfFile/CreateFileMappingW.
|
||||
unsafe {
|
||||
if let Some(h) = self.hsw {
|
||||
SwDeviceClose(h);
|
||||
}
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: self.view as *mut c_void,
|
||||
});
|
||||
let _ = CloseHandle(self.map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub struct GamepadManager {
|
||||
pads: Vec<Option<XusbWinPad>>,
|
||||
last_rumble: Vec<(u8, u8)>,
|
||||
broken: bool,
|
||||
}
|
||||
|
||||
impl Default for GamepadManager {
|
||||
fn default() -> GamepadManager {
|
||||
GamepadManager::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GamepadManager {
|
||||
pub fn new() -> GamepadManager {
|
||||
GamepadManager {
|
||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
||||
last_rumble: vec![(0, 0); MAX_PADS],
|
||||
broken: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure(&mut self, idx: usize) {
|
||||
if idx >= MAX_PADS || self.pads[idx].is_some() || self.broken {
|
||||
return;
|
||||
}
|
||||
match XusbWinPad::open(idx as u8) {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual Xbox 360 created (Windows XUSB companion)"
|
||||
);
|
||||
self.pads[idx] = Some(p);
|
||||
self.last_rumble[idx] = (0, 0);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %format!("{e:#}"), "virtual Xbox 360 creation failed — controller input disabled (is the pf_xusb driver installed?)");
|
||||
self.broken = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, ev: &GamepadEvent) {
|
||||
let GamepadEvent::State(f) = ev else {
|
||||
return; // Arrival metadata — the pad is created lazily on the first State
|
||||
};
|
||||
let idx = f.index.max(0) as usize;
|
||||
if idx >= MAX_PADS {
|
||||
return;
|
||||
}
|
||||
// Unplugs: drop any allocated pad whose mask bit cleared.
|
||||
for (i, slot) in self.pads.iter_mut().enumerate() {
|
||||
if slot.is_some() && f.active_mask & (1 << i) == 0 {
|
||||
tracing::info!(index = i, "controller unplugged (Xbox 360/Windows)");
|
||||
*slot = None;
|
||||
self.last_rumble[i] = (0, 0);
|
||||
}
|
||||
}
|
||||
if f.active_mask & (1 << idx) == 0 {
|
||||
return;
|
||||
}
|
||||
self.ensure(idx);
|
||||
if let Some(pad) = self.pads[idx].as_mut() {
|
||||
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 in 0..self.pads.len() {
|
||||
let Some(pad) = self.pads[i].as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some((large, small)) = pad.service() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
//! 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. The client already sends Windows VK codes, so there is no
|
||||
//! keycode table. 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.
|
||||
|
||||
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::{
|
||||
MapVirtualKeyExW, SendInput, 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::{
|
||||
GetSystemMetrics, 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>,
|
||||
}
|
||||
|
||||
// Only ever used from the host's single injector thread (like SudoVdaDisplay).
|
||||
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) {
|
||||
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<()> {
|
||||
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();
|
||||
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() {
|
||||
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; // client sends Windows VK
|
||||
let sc_ex = unsafe { MapVirtualKeyExW(vk as u32, MAPVK_VK_TO_VSC_EX, None) };
|
||||
if sc_ex == 0 {
|
||||
return Ok(()); // unmappable -> drop
|
||||
}
|
||||
let extended = (sc_ex & 0xe000) == 0xe000 || forced_extended(vk);
|
||||
let scan = (sc_ex & 0xff) as u16;
|
||||
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 ViGEm (separate backend). Touch: no SendInput equivalent -> no-op.
|
||||
InputKind::GamepadButton
|
||||
| InputKind::GamepadAxis
|
||||
| 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) {
|
||||
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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user