refactor(windows-host): shared Shm/SwDevice RAII for the 3 gamepad backends (Goal-3 unsafe reduction)
The DualSense, DualShock 4, and XUSB Windows pad backends each hand-rolled the SAME per-pad resource handling: a `CreateFileMappingW` + `MapViewOfFile` shared section (with the permissive D:(A;;GA;;;WD) SDDL the restricted-token driver needs) and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` — three copies, each a chance to drift or leak on an error path. New `inject/windows/gamepad_raii.rs` owns both resources with RAII: - `Shm` — the section handle (`OwnedHandle`) + its view; `Shm::create(name, size)` does the SDDL + map + zero-fill leak-safely, `base()` gives the mapped pointer, `Drop` unmaps then closes (in that order). - `SwDevice` — the `SwDeviceCreate`'d devnode; `Drop` calls `SwDeviceClose`. All three backends now hold `_sw: Option<SwDevice>` + `shm: Shm` instead of raw `hsw`/`map`/`view`, access the section via `self.shm.base()`, and have NO manual `Drop`. Deletes the duplicated `create_shm_section` (DualSense/DS4 now use `Shm::create`) and the three hand-written Drops; the DS4 device-type byte is still written before the magic, the SwDeviceCreate `None` fallback still works, and the field drop order (devnode removed, then section unmapped+closed) matches the old manual order. Net: 3 manual `Drop`s + a duplicated section-creation path → one shared RAII module; fewer unsafe ops, leak-on-error fixed by construction. Linux `cargo check` clean (the inject mod wiring); the backends are #[cfg(windows)] → CI-gated. Drafted + adversarially verified (no double-free, imports correct under -D warnings, behavior preserved); my own spot-checks confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -459,6 +459,11 @@ pub mod gamepad;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "inject/windows/gamepad_windows.rs"]
|
#[path = "inject/windows/gamepad_windows.rs"]
|
||||||
pub mod gamepad;
|
pub mod gamepad;
|
||||||
|
/// Windows: small RAII wrappers (`Shm` section+view, `SwDevice` devnode) shared by the three gamepad
|
||||||
|
/// backends (DualSense / DualShock 4 / XUSB), so each per-pad resource closes deterministically on drop.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[path = "inject/windows/gamepad_raii.rs"]
|
||||||
|
mod gamepad_raii;
|
||||||
/// Stub — virtual gamepads need Linux uinput or the Windows UMDF drivers; events are dropped elsewhere.
|
/// Stub — virtual gamepads need Linux uinput or the Windows UMDF drivers; events are dropped elsewhere.
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
pub mod gamepad {
|
pub mod gamepad {
|
||||||
|
|||||||
@@ -29,15 +29,7 @@ use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
|||||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||||
};
|
};
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
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};
|
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
|
||||||
|
|
||||||
/// Shared-section layout — the single source of truth is [`pf_driver_proto::gamepad::PadShm`] (offset
|
/// Shared-section layout — the single source of truth is [`pf_driver_proto::gamepad::PadShm`] (offset
|
||||||
@@ -60,11 +52,11 @@ pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUAL
|
|||||||
/// loads on it and the HID DualSense appears to games) plus the shared-memory section the driver maps.
|
/// 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.
|
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||||
struct DsWinPad {
|
struct DsWinPad {
|
||||||
/// Per-session devnode from SwDeviceCreate, when it succeeds. `None` falls back to an out-of-band
|
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||||
/// `pf_dualsense` devnode (installer/devgen).
|
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
||||||
hsw: Option<HSWDEVICE>,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
map: HANDLE,
|
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||||
view: *mut u8,
|
shm: super::gamepad_raii::Shm,
|
||||||
seq: u8,
|
seq: u8,
|
||||||
ts: u32,
|
ts: u32,
|
||||||
last_out_seq: u32,
|
last_out_seq: u32,
|
||||||
@@ -238,62 +230,16 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<HSWDEVICE> {
|
|||||||
Ok(hsw)
|
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_driver_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 {
|
impl DsWinPad {
|
||||||
/// Create + map the section `Global\pfds-shm-<index>`, stamp the magic, then spawn the
|
/// 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
|
/// `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`).
|
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||||
fn open(index: u8) -> Result<DsWinPad> {
|
fn open(index: u8) -> Result<DsWinPad> {
|
||||||
let (map, base) = create_shm_section(index)?;
|
let shm = super::gamepad_raii::Shm::create(
|
||||||
|
&HSTRING::from(pf_driver_proto::gamepad::pad_shm_name(index)),
|
||||||
|
SHM_SIZE,
|
||||||
|
)?;
|
||||||
|
let base = shm.base();
|
||||||
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
|
// 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).
|
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
|
||||||
// SAFETY: base points at SHM_SIZE writable bytes.
|
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||||
@@ -322,10 +268,10 @@ impl DsWinPad {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||||
Ok(DsWinPad {
|
Ok(DsWinPad {
|
||||||
hsw,
|
_sw,
|
||||||
map,
|
shm,
|
||||||
view: base,
|
|
||||||
seq: 0,
|
seq: 0,
|
||||||
ts: 0,
|
ts: 0,
|
||||||
last_out_seq: 0,
|
last_out_seq: 0,
|
||||||
@@ -338,22 +284,25 @@ impl DsWinPad {
|
|||||||
self.ts = self.ts.wrapping_add(1);
|
self.ts = self.ts.wrapping_add(1);
|
||||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||||
serialize_state(&mut r, st, self.seq, self.ts);
|
serialize_state(&mut r, st, self.seq, self.ts);
|
||||||
// SAFETY: view points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
// SAFETY: base 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()) };
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll the section's output slot; parse a new `0x02` report (rumble / LEDs / triggers) into a
|
/// 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.
|
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything new.
|
||||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||||
let mut fb = DsFeedback::default();
|
let mut fb = DsFeedback::default();
|
||||||
// SAFETY: view points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_OUT_SEQ) as *const u32) };
|
let seq =
|
||||||
|
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||||
if seq != self.last_out_seq {
|
if seq != self.last_out_seq {
|
||||||
self.last_out_seq = seq;
|
self.last_out_seq = seq;
|
||||||
let mut out = [0u8; 64];
|
let mut out = [0u8; 64];
|
||||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(self.view.add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||||
};
|
};
|
||||||
parse_ds_output(pad, &out, &mut fb);
|
parse_ds_output(pad, &out, &mut fb);
|
||||||
}
|
}
|
||||||
@@ -361,21 +310,6 @@ impl DsWinPad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// All virtual DualSense pads of a session — the Windows analogue of
|
||||||
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface so the session input
|
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface so the session input
|
||||||
/// thread drives either backend identically.
|
/// thread drives either backend identically.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
use super::dualsense_proto::DsState;
|
use super::dualsense_proto::DsState;
|
||||||
use super::dualsense_windows::{
|
use super::dualsense_windows::{
|
||||||
create_shm_section, create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE,
|
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_INPUT, OFF_OUTPUT,
|
||||||
OFF_INPUT, OFF_OUTPUT, OFF_OUT_SEQ, SHM_MAGIC,
|
OFF_OUT_SEQ, SHM_MAGIC, SHM_SIZE,
|
||||||
};
|
};
|
||||||
use super::dualshock4_proto::{
|
use super::dualshock4_proto::{
|
||||||
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
|
||||||
@@ -18,18 +18,16 @@ use super::dualshock4_proto::{
|
|||||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||||
use std::ffi::c_void;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
use windows::core::HSTRING;
|
||||||
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
|
/// 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.
|
/// shared section. Dropping it removes the devnode and unmaps + closes the section.
|
||||||
struct Ds4WinPad {
|
struct Ds4WinPad {
|
||||||
hsw: Option<HSWDEVICE>,
|
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||||
map: HANDLE,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
view: *mut u8,
|
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
|
||||||
|
shm: super::gamepad_raii::Shm,
|
||||||
counter: u8,
|
counter: u8,
|
||||||
ts: u16,
|
ts: u16,
|
||||||
last_out_seq: u32,
|
last_out_seq: u32,
|
||||||
@@ -39,7 +37,11 @@ impl Ds4WinPad {
|
|||||||
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
|
/// 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).
|
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
|
||||||
fn open(index: u8) -> Result<Ds4WinPad> {
|
fn open(index: u8) -> Result<Ds4WinPad> {
|
||||||
let (map, base) = create_shm_section(index)?;
|
let shm = super::gamepad_raii::Shm::create(
|
||||||
|
&HSTRING::from(pf_driver_proto::gamepad::pad_shm_name(index)),
|
||||||
|
SHM_SIZE,
|
||||||
|
)?;
|
||||||
|
let base = shm.base();
|
||||||
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
|
// 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.
|
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -65,10 +67,10 @@ impl Ds4WinPad {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||||
Ok(Ds4WinPad {
|
Ok(Ds4WinPad {
|
||||||
hsw,
|
_sw,
|
||||||
map,
|
shm,
|
||||||
view: base,
|
|
||||||
counter: 0,
|
counter: 0,
|
||||||
ts: 0,
|
ts: 0,
|
||||||
last_out_seq: 0,
|
last_out_seq: 0,
|
||||||
@@ -81,22 +83,25 @@ impl Ds4WinPad {
|
|||||||
self.ts = self.ts.wrapping_add(188); // ~1ms in the DS4's 5.33µs sensor-clock units
|
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];
|
let mut r = [0u8; DS4_INPUT_REPORT_LEN];
|
||||||
serialize_state(&mut r, st, self.counter, self.ts);
|
serialize_state(&mut r, st, self.counter, self.ts);
|
||||||
// SAFETY: view points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
// SAFETY: base 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()) };
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(r.as_ptr(), self.shm.base().add(OFF_INPUT), r.len())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll the section's output slot; parse a new `0x05` report (rumble / lightbar) into a
|
/// 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.
|
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new.
|
||||||
fn service(&mut self) -> Ds4Feedback {
|
fn service(&mut self) -> Ds4Feedback {
|
||||||
let mut fb = Ds4Feedback::default();
|
let mut fb = Ds4Feedback::default();
|
||||||
// SAFETY: view points at SHM_SIZE bytes.
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_OUT_SEQ) as *const u32) };
|
let seq =
|
||||||
|
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
|
||||||
if seq != self.last_out_seq {
|
if seq != self.last_out_seq {
|
||||||
self.last_out_seq = seq;
|
self.last_out_seq = seq;
|
||||||
let mut out = [0u8; 64];
|
let mut out = [0u8; 64];
|
||||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(self.view.add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
std::ptr::copy_nonoverlapping(self.shm.base().add(OFF_OUTPUT), out.as_mut_ptr(), 64)
|
||||||
};
|
};
|
||||||
parse_ds4_output(&out, &mut fb);
|
parse_ds4_output(&out, &mut fb);
|
||||||
}
|
}
|
||||||
@@ -104,21 +109,6 @@ impl Ds4WinPad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// All virtual DualShock 4 pads of a session — the Windows analogue of
|
||||||
/// [`DualShock4Manager`](super::dualshock4::DualShock4Manager), with the same method surface as the
|
/// [`DualShock4Manager`](super::dualshock4::DualShock4Manager), with the same method surface as the
|
||||||
/// Windows DualSense manager so the session input thread drives either backend identically.
|
/// Windows DualSense manager so the session input thread drives either backend identically.
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
//! Per-pad Windows resource RAII for the gamepad backends (DualSense / DualShock 4 / XUSB).
|
||||||
|
//!
|
||||||
|
//! Each virtual pad owns two OS resources: the named shared-memory section (+ its mapped view) the
|
||||||
|
//! `pf_dualsense`/`pf_xusb` driver reads, and the `SwDeviceCreate`'d software devnode the driver loads
|
||||||
|
//! on. Before this module, all three backends hand-rolled the same `CreateFileMappingW` +
|
||||||
|
//! `MapViewOfFile` and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` + `CloseHandle` —
|
||||||
|
//! easy to drift or leak on an error path. [`Shm`] and [`SwDevice`] own those resources with RAII, so a
|
||||||
|
//! backend just holds them and the cleanup (and ordering) happens by construction.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::os::windows::io::{FromRawHandle, OwnedHandle};
|
||||||
|
use windows::core::{w, HSTRING, PCWSTR};
|
||||||
|
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
|
||||||
|
use windows::Win32::Foundation::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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A named, anonymous (pagefile-backed) shared section + its mapped read/write view, created with the
|
||||||
|
/// permissive `D:(A;;GA;;;WD)` SDDL the restricted-token driver needs to open it. RAII: drop unmaps the
|
||||||
|
/// view, then the [`OwnedHandle`] closes the section handle (in that order). Replaces the three backends'
|
||||||
|
/// hand-duplicated `CreateFileMappingW` + `MapViewOfFile` + manual `Drop`.
|
||||||
|
pub(super) struct Shm {
|
||||||
|
/// Owns the section handle (closed on drop). Held only for ownership — never read after construction.
|
||||||
|
_handle: OwnedHandle,
|
||||||
|
view: MEMORY_MAPPED_VIEW_ADDRESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shm {
|
||||||
|
/// Create + zero a `size`-byte section named `name`, mapped read/write. The section handle is owned
|
||||||
|
/// immediately, so any failure below (or the returned `Shm`'s drop) closes it.
|
||||||
|
pub(super) fn create(name: &HSTRING, size: usize) -> Result<Shm> {
|
||||||
|
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||||
|
// SAFETY: the SDDL literal is valid; `psd` receives an OS-allocated descriptor (freed at process
|
||||||
|
// exit — acceptable for a host-lifetime object).
|
||||||
|
unsafe {
|
||||||
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||||
|
w!("D:(A;;GA;;;WD)"),
|
||||||
|
SDDL_REVISION_1,
|
||||||
|
&mut psd,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
let sa = SECURITY_ATTRIBUTES {
|
||||||
|
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||||
|
lpSecurityDescriptor: psd.0,
|
||||||
|
bInheritHandle: false.into(),
|
||||||
|
};
|
||||||
|
// SAFETY: an anonymous (pagefile-backed) section of `size` bytes with the SDDL above.
|
||||||
|
let map = unsafe {
|
||||||
|
CreateFileMappingW(
|
||||||
|
INVALID_HANDLE_VALUE,
|
||||||
|
Some(&sa),
|
||||||
|
PAGE_READWRITE,
|
||||||
|
0,
|
||||||
|
size as u32,
|
||||||
|
PCWSTR(name.as_ptr()),
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
// 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 for {name}"));
|
||||||
|
}
|
||||||
|
// SAFETY: `view` points at `size` writable bytes (just mapped).
|
||||||
|
unsafe { core::ptr::write_bytes(view.Value as *mut u8, 0, size) };
|
||||||
|
Ok(Shm {
|
||||||
|
_handle: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,15 +21,7 @@ use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
|
|||||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||||
};
|
};
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
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};
|
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
|
||||||
|
|
||||||
// Shared-section layout — the single source of truth is `pf_driver_proto::gamepad::XusbShm` (offset
|
// Shared-section layout — the single source of truth is `pf_driver_proto::gamepad::XusbShm` (offset
|
||||||
@@ -150,9 +142,10 @@ fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
|
|||||||
|
|
||||||
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
|
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
|
||||||
struct XusbWinPad {
|
struct XusbWinPad {
|
||||||
hsw: Option<HSWDEVICE>,
|
/// Owns the `pf_xusb_<index>` devnode (dropped → `SwDeviceClose`). `None` if `SwDeviceCreate` failed.
|
||||||
map: HANDLE,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
view: *mut u8,
|
/// Owns `Global\pfxusb-shm-<index>` (the section + its mapped view; drop unmaps + closes).
|
||||||
|
shm: super::gamepad_raii::Shm,
|
||||||
packet: u32,
|
packet: u32,
|
||||||
last_rumble_seq: u32,
|
last_rumble_seq: u32,
|
||||||
}
|
}
|
||||||
@@ -160,45 +153,13 @@ struct XusbWinPad {
|
|||||||
impl XusbWinPad {
|
impl XusbWinPad {
|
||||||
/// Create + map `Global\pfxusb-shm-<index>`, stamp the magic, then spawn the devnode.
|
/// Create + map `Global\pfxusb-shm-<index>`, stamp the magic, then spawn the devnode.
|
||||||
fn open(index: u8) -> Result<XusbWinPad> {
|
fn open(index: u8) -> Result<XusbWinPad> {
|
||||||
let name = HSTRING::from(pf_driver_proto::gamepad::xusb_shm_name(index));
|
// Permissive-DACL named section the WUDFHost (whatever account) can open; `Shm` owns the
|
||||||
|
// section handle + its mapped view (zero-filled) and unmaps/closes on drop.
|
||||||
// Permissive DACL so the WUDFHost (whatever account) can open the section.
|
let shm = super::gamepad_raii::Shm::create(
|
||||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
&HSTRING::from(pf_driver_proto::gamepad::xusb_shm_name(index)),
|
||||||
// SAFETY: SDDL literal valid; psd receives an OS-freed descriptor (host-lifetime — fine).
|
SHM_SIZE,
|
||||||
unsafe {
|
)?;
|
||||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
let base = shm.base();
|
||||||
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).
|
// 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.
|
// SAFETY: base points at SHM_SIZE writable bytes.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -212,10 +173,10 @@ impl XusbWinPad {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||||
Ok(XusbWinPad {
|
Ok(XusbWinPad {
|
||||||
hsw,
|
_sw,
|
||||||
map,
|
shm,
|
||||||
view: base,
|
|
||||||
packet: 0,
|
packet: 0,
|
||||||
last_rumble_seq: 0,
|
last_rumble_seq: 0,
|
||||||
})
|
})
|
||||||
@@ -226,50 +187,36 @@ impl XusbWinPad {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) {
|
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);
|
self.packet = self.packet.wrapping_add(1);
|
||||||
// SAFETY: view points at SHM_SIZE bytes; all offsets are in range.
|
// SAFETY: base points at SHM_SIZE bytes; all offsets are in range.
|
||||||
|
let base = self.shm.base();
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_BUTTONS) as *mut u16, buttons);
|
std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons);
|
||||||
*self.view.add(OFF_LT) = lt;
|
*base.add(OFF_LT) = lt;
|
||||||
*self.view.add(OFF_RT) = rt;
|
*base.add(OFF_RT) = rt;
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_LX) as *mut i16, lx);
|
std::ptr::write_unaligned(base.add(OFF_LX) as *mut i16, lx);
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_LY) as *mut i16, ly);
|
std::ptr::write_unaligned(base.add(OFF_LY) as *mut i16, ly);
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_RX) as *mut i16, rx);
|
std::ptr::write_unaligned(base.add(OFF_RX) as *mut i16, rx);
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_RY) as *mut i16, ry);
|
std::ptr::write_unaligned(base.add(OFF_RY) as *mut i16, ry);
|
||||||
std::ptr::write_unaligned(self.view.add(OFF_PACKET) as *mut u32, self.packet);
|
std::ptr::write_unaligned(base.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
|
/// 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.
|
/// `(large, small)` motor levels (0..=255) when a new one arrived.
|
||||||
fn service(&mut self) -> Option<(u8, u8)> {
|
fn service(&mut self) -> Option<(u8, u8)> {
|
||||||
// SAFETY: view points at SHM_SIZE bytes.
|
let base = self.shm.base();
|
||||||
let seq = unsafe { std::ptr::read_unaligned(self.view.add(OFF_RUMBLE_SEQ) as *const u32) };
|
// SAFETY: base points at SHM_SIZE bytes.
|
||||||
|
let seq = unsafe { std::ptr::read_unaligned(base.add(OFF_RUMBLE_SEQ) as *const u32) };
|
||||||
if seq == self.last_rumble_seq {
|
if seq == self.last_rumble_seq {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.last_rumble_seq = seq;
|
self.last_rumble_seq = seq;
|
||||||
// SAFETY: rumble bytes at OFF_RUMBLE / OFF_RUMBLE+1.
|
// SAFETY: rumble bytes at OFF_RUMBLE / OFF_RUMBLE+1.
|
||||||
let (large, small) =
|
let (large, small) = unsafe { (*base.add(OFF_RUMBLE), *base.add(OFF_RUMBLE + 1)) };
|
||||||
unsafe { (*self.view.add(OFF_RUMBLE), *self.view.add(OFF_RUMBLE + 1)) };
|
|
||||||
Some((large, small))
|
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,
|
/// 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
|
/// now backed by the XUSB companion driver. Same method surface (`new`/`handle`/`pump_rumble`) the
|
||||||
/// session input thread already drives.
|
/// session input thread already drives.
|
||||||
|
|||||||
Reference in New Issue
Block a user