feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)
Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\ objects (openable by any sibling LocalService) to UNNAMED sections/events whose handles the host DuplicateHandles into the driver's verified WUDFHost with least access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL, pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded; HID minidrivers have no control device). Driver-validated pad_index kills cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides. Sibling-LocalService denial proven empirically (design/idd-push-security.md, design/gamepad-channel-sealing.md). Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI. driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created SwDevice REVIVES the old devnode with its previously-bound driver (never re-ranks), so an upgrade otherwise leaves the old driver serving — or, across the v1→v2 fence, a dead pad (found live on the RTX box). On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via both the test harness and a real streaming session; phantom-sweep repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+12
@@ -405,11 +405,21 @@ dependencies = [
|
||||
name = "pf-dualsense"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"pf-driver-proto",
|
||||
"pf-umdf-util",
|
||||
"wdk",
|
||||
"wdk-build",
|
||||
"wdk-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-umdf-util"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"pf-driver-proto",
|
||||
"wdk-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-vdisplay"
|
||||
version = "0.0.1"
|
||||
@@ -427,6 +437,8 @@ dependencies = [
|
||||
name = "pf-xusb"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"pf-driver-proto",
|
||||
"pf-umdf-util",
|
||||
"wdk",
|
||||
"wdk-build",
|
||||
"wdk-sys",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# crates/pf-driver-proto from the main tree.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["wdk-probe", "wdk-iddcx", "pf-vdisplay", "pf-dualsense", "pf-xusb"]
|
||||
members = ["wdk-probe", "wdk-iddcx", "pf-umdf-util", "pf-vdisplay", "pf-dualsense", "pf-xusb"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
@@ -20,6 +20,7 @@ wdk = "0.4.1"
|
||||
wdk-sys = "0.5.1"
|
||||
wdk-build = "0.5.1"
|
||||
wdk-iddcx = { path = "wdk-iddcx" }
|
||||
pf-umdf-util = { path = "pf-umdf-util" }
|
||||
pf-driver-proto = { path = "../../../crates/pf-driver-proto" }
|
||||
|
||||
# Vendored windows-drivers-rs 0.5.1 (the published, self-contained crates) + an added `iddcx`
|
||||
|
||||
@@ -23,6 +23,8 @@ wdk-build.workspace = true
|
||||
[dependencies]
|
||||
wdk.workspace = true
|
||||
wdk-sys.workspace = true
|
||||
pf-driver-proto.workspace = true
|
||||
pf-umdf-util.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["hid"]
|
||||
|
||||
@@ -85,6 +85,9 @@ silently breaks them:
|
||||
|
||||
- **Multi-pad** works via `UmdfHostProcessSharing=ProcessSharingDisabled` — each pad gets its own
|
||||
WUDFHost (so the per-instance statics don't collide), and the driver reads its pad index from the
|
||||
device Location (`WdfDeviceAllocAndQueryProperty`) to map its own `*-shm-<index>` channel.
|
||||
device Location (`WdfDeviceAllocAndQueryProperty`) to poll its own `*-boot-<index>` bootstrap
|
||||
mailbox (the DATA section itself is unnamed — the sealed pad channel,
|
||||
`design/gamepad-channel-sealing.md` — and its `pad_index` is validated against this index on
|
||||
attach).
|
||||
- Port of the WDK `vhidmini2` UMDF2 sample; the DualSense identity + 273-byte descriptor + feature
|
||||
blobs `0x05`/`0x09`/`0x20` come from `crates/punktfunk-host/src/inject/dualsense.rs`.
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
// punktfunk virtual DualSense — UMDF2 HID minidriver (M0 spike).
|
||||
// punktfunk virtual DualSense / DualShock 4 — UMDF2 HID minidriver.
|
||||
//
|
||||
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
|
||||
// (VID 054C / PID 0CE6) using the inputtino report descriptor + feature blobs punktfunk already
|
||||
// ships in `inject/dualsense.rs`. Its purpose for M0(b) is to (1) enumerate as a genuine DualSense
|
||||
// and (2) LOG every output report the game writes — the adaptive-trigger `0x02` gate.
|
||||
// (VID 054C / PID 0CE6) or DualShock 4 (device_type=1) using the inputtino report descriptor +
|
||||
// feature blobs punktfunk already ships in `inject/{dualsense,dualshock4}.rs`. Games see a genuine
|
||||
// HID PS controller; the host streams input in / reads output (rumble/lightbar/triggers) back.
|
||||
//
|
||||
// No WDF object contexts: this is a singleton virtual device, so per-device state lives in statics.
|
||||
// All WDF calls go through `call_unsafe_wdf_function_binding!`; HID/WDF structs are hand-built.
|
||||
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
|
||||
// whole handshake + all shared-memory access lives in `pf_umdf_util` (the audited unsafe layer), so
|
||||
// this crate's channel/HID/IOCTL logic is 100% SAFE Rust. The only `unsafe` here is the unavoidable
|
||||
// WDF setup FFI in DriverEntry/EvtDeviceAdd/the timer, each with a `// SAFETY:` proof.
|
||||
|
||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
||||
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
|
||||
|
||||
use pf_driver_proto::gamepad::PadShm;
|
||||
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
|
||||
use pf_umdf_util::wdf::{self, Request};
|
||||
use wdk_sys::{
|
||||
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
||||
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
|
||||
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
|
||||
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
|
||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
||||
};
|
||||
|
||||
// ---- NTSTATUS values ----
|
||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||
const STATUS_UNSUCCESSFUL: NTSTATUS = 0xC000_0001u32 as NTSTATUS;
|
||||
const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
||||
const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
use pf_umdf_util::nt_success;
|
||||
|
||||
// ---- HID minidriver IOCTLs: CTL_CODE(FILE_DEVICE_KEYBOARD=0x0b, id, METHOD_NEITHER=3, ANY) ----
|
||||
const fn hid_ctl(id: u32) -> u32 {
|
||||
@@ -225,26 +228,45 @@ static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut(
|
||||
/// to pended game READ_REPORTs. Defaults to neutral until the host connects.
|
||||
static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
|
||||
|
||||
// ---- user-mode shared-memory IPC with the punktfunk host ----
|
||||
// ---- the sealed pad channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
|
||||
// UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
|
||||
// (custom interface CreateFile → err 31; custom IOCTL on the HID handle → err 1) and UMDF has no
|
||||
// control device, so the host channel is a named section the (privileged) host CREATES and the driver
|
||||
// OPENS. Layout (256 B, must match pf_driver_proto::gamepad::PadShm): magic u32 @0 ("PFDS"),
|
||||
// input_seq u32 @4, input_report[64] @8, output_seq u32 @72, output_report[64] @76,
|
||||
// device_type u8 @140, driver_proto u32 @144 (we stamp GAMEPAD_PROTO_VERSION = the host's
|
||||
// driver-attach health signal), driver_heartbeat u32 @148 (we bump per timer tick = liveness).
|
||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
|
||||
const SHM_MAGIC: u32 = 0x5046_4453; // "PFDS" little-endian
|
||||
const SHM_SIZE: usize = 256;
|
||||
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
|
||||
static LOGGED_SHM: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
|
||||
// control device. So the DATA section (`PadShm`, 256 B — input report @8, output seq @72, output
|
||||
// report @76, device_type @140, health marks @144/@148, pad_index @152) is UNNAMED and reached only
|
||||
// through a handle the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named mailbox
|
||||
// `Global\pfds-boot-<index>`. The handshake + all shared-memory access live in `pf_umdf_util`.
|
||||
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS"
|
||||
const SHM_SIZE: usize = core::mem::size_of::<PadShm>();
|
||||
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
|
||||
|
||||
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
|
||||
unsafe extern "system" {
|
||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||
fn CloseHandle(h: *mut c_void) -> i32;
|
||||
// PadShm field offsets (the driver reads input + device_type, writes output + health marks).
|
||||
const OFF_INPUT: usize = core::mem::offset_of!(PadShm, input);
|
||||
const OFF_OUT_SEQ: usize = core::mem::offset_of!(PadShm, out_seq);
|
||||
const OFF_OUTPUT: usize = core::mem::offset_of!(PadShm, output);
|
||||
const OFF_DEVICE_TYPE: usize = core::mem::offset_of!(PadShm, device_type);
|
||||
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(PadShm, driver_proto);
|
||||
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(PadShm, driver_heartbeat);
|
||||
const OFF_PAD_INDEX: usize = core::mem::offset_of!(PadShm, pad_index);
|
||||
|
||||
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
|
||||
/// this static is per-pad). The handshake/adoption/validation state machine lives in `pf_umdf_util`.
|
||||
static CHANNEL: ChannelClient = ChannelClient::new();
|
||||
/// The last observed `device_type` (0 = DualSense, 1 = DualShock 4) — the neutral-report shape when
|
||||
/// the channel detaches, and the fallback identity while unattached.
|
||||
static LAST_DEVTYPE: AtomicU32 = AtomicU32::new(0);
|
||||
/// device_type()'s bounded first-read wait fires at most once (see its docs).
|
||||
static DEVTYPE_WAITED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// This pad's channel config (magic/size/pad_index offset + our logger).
|
||||
fn channel_cfg() -> ChannelConfig {
|
||||
ChannelConfig {
|
||||
tag: "pf-ds",
|
||||
boot_name_prefix: "Global\\pfds-boot-",
|
||||
data_magic: SHM_MAGIC,
|
||||
data_size: SHM_SIZE,
|
||||
pad_index_off: OFF_PAD_INDEX,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
fn log(s: &str) {
|
||||
@@ -289,59 +311,6 @@ pub unsafe extern "system" fn driver_entry(
|
||||
}
|
||||
}
|
||||
|
||||
/// The pad index this device serves (which `pfds-shm-<index>` section to map). The host stamps it into
|
||||
/// the device Location (`pszDeviceLocation`); the driver reads it in EvtDeviceAdd. With
|
||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) each pad gets its own WUDFHost, so this
|
||||
/// static is per-pad — the basis for multi-pad.
|
||||
static SHM_INDEX: AtomicU32 = AtomicU32::new(0);
|
||||
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (not re-exported at the wdk_sys root).
|
||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||
|
||||
/// Read the pad index the host stamped into the device Location (a NUL-terminated UTF-16 decimal
|
||||
/// string). Defaults to 0 (single-pad) if absent.
|
||||
fn query_shm_index(device: WDFDEVICE) -> u32 {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: device valid; property = LocationInformation; pool ignored in UMDF; mem receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceAllocAndQueryProperty,
|
||||
device,
|
||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
||||
0,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut mem
|
||||
)
|
||||
};
|
||||
if !nt_success(st) || mem.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: mem valid.
|
||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
||||
as *const u16;
|
||||
if buf.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut idx: u32 = 0;
|
||||
let mut any = false;
|
||||
for i in 0..(len / 2).min(8) {
|
||||
// SAFETY: buf valid for len bytes; i < len/2.
|
||||
let c = unsafe { *buf.add(i) };
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
if (0x30..=0x39).contains(&c) {
|
||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any {
|
||||
idx
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
||||
log("[pf-ds] EvtDeviceAdd");
|
||||
|
||||
@@ -364,8 +333,9 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
return st;
|
||||
}
|
||||
|
||||
let shm_idx = query_shm_index(device);
|
||||
SHM_INDEX.store(shm_idx, Ordering::Relaxed);
|
||||
// SAFETY: `device` is the live device just created — the exact contract this fn requires.
|
||||
let shm_idx = unsafe { wdf::query_location_index(device) };
|
||||
CHANNEL.set_index(shm_idx);
|
||||
dbglog!("[pf-ds] shm index = {shm_idx}");
|
||||
|
||||
// Default parallel queue handling all IOCTLs.
|
||||
@@ -428,6 +398,8 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
tcfg.EvtTimerFunc = Some(evt_timer);
|
||||
tcfg.Period = 8; // ms
|
||||
tcfg.AutomaticSerialization = 1; // TRUE — UMDF requires a serialized timer (vhidmini2 pattern)
|
||||
// SAFETY: a zeroed WDF_OBJECT_ATTRIBUTES is a valid all-null attributes struct; we set Size + the
|
||||
// fields we use below.
|
||||
let mut tattr: WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
|
||||
tattr.Size = core::mem::size_of::<WDF_OBJECT_ATTRIBUTES>() as ULONG;
|
||||
tattr.ParentObject = manual_queue.cast();
|
||||
@@ -458,140 +430,72 @@ extern "C" fn evt_io_device_control(
|
||||
_input_len: usize,
|
||||
ioctl: ULONG,
|
||||
) {
|
||||
let mut complete = true;
|
||||
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
|
||||
// contract `Request::new` requires. Everything after is safe (the token owns completion).
|
||||
let request = unsafe { Request::new(request) };
|
||||
|
||||
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test;
|
||||
// the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
|
||||
if ioctl != IOCTL_HID_READ_REPORT {
|
||||
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
|
||||
}
|
||||
|
||||
// READ_REPORT forwards to the manual queue (the timer completes it) — this CONSUMES the request
|
||||
// token, so it's handled apart from the status-and-complete paths below.
|
||||
if ioctl == IOCTL_HID_READ_REPORT {
|
||||
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
|
||||
// SAFETY: `mq` is the manual queue created in EvtDeviceAdd (a live WDFQUEUE of this device).
|
||||
match unsafe { request.forward_to_queue(mq) } {
|
||||
Ok(()) => {} // framework owns it now (completed by the timer)
|
||||
Err((req, st)) => req.complete(st), // forward failed → complete with the error
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let status: NTSTATUS = match ioctl {
|
||||
IOCTL_HID_GET_DEVICE_DESCRIPTOR => {
|
||||
copy_to_output(request, if device_type() == 1 { &DS4_HID_DESC } else { &HID_DESC })
|
||||
}
|
||||
IOCTL_HID_GET_DEVICE_ATTRIBUTES => copy_to_output(request, &hid_attrs(device_type() == 1)),
|
||||
IOCTL_HID_GET_REPORT_DESCRIPTOR => copy_to_output(
|
||||
request,
|
||||
if device_type() == 1 {
|
||||
&DS4_RDESC[..]
|
||||
} else {
|
||||
&DUALSENSE_RDESC[..]
|
||||
},
|
||||
),
|
||||
IOCTL_HID_READ_REPORT => {
|
||||
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
|
||||
// SAFETY: request valid; mq is the manual queue created in EvtDeviceAdd.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, request, mq)
|
||||
};
|
||||
if nt_success(st) {
|
||||
complete = false;
|
||||
STATUS_SUCCESS
|
||||
} else {
|
||||
st
|
||||
}
|
||||
}
|
||||
IOCTL_HID_GET_DEVICE_DESCRIPTOR => request.copy_to_output(if device_type() == 1 {
|
||||
&DS4_HID_DESC
|
||||
} else {
|
||||
&HID_DESC
|
||||
}),
|
||||
IOCTL_HID_GET_DEVICE_ATTRIBUTES => request.copy_to_output(&hid_attrs(device_type() == 1)),
|
||||
IOCTL_HID_GET_REPORT_DESCRIPTOR => request.copy_to_output(if device_type() == 1 {
|
||||
&DS4_RDESC[..]
|
||||
} else {
|
||||
&DUALSENSE_RDESC[..]
|
||||
}),
|
||||
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
|
||||
on_output_report(request, ioctl)
|
||||
on_output_report(&request, ioctl)
|
||||
}
|
||||
IOCTL_UMDF_HID_SET_FEATURE => {
|
||||
log("[pf-ds] SET_FEATURE (stub ok)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request),
|
||||
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(&request),
|
||||
IOCTL_UMDF_HID_GET_INPUT_REPORT => {
|
||||
copy_to_output(request, &neutral_report(device_type() == 1))
|
||||
request.copy_to_output(&neutral_report(device_type() == 1))
|
||||
}
|
||||
IOCTL_HID_GET_STRING => on_get_string(request),
|
||||
IOCTL_HID_GET_STRING => on_get_string(&request),
|
||||
_ => STATUS_NOT_IMPLEMENTED,
|
||||
};
|
||||
|
||||
if ioctl != IOCTL_HID_READ_REPORT {
|
||||
dbglog!(
|
||||
"[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}",
|
||||
status as u32
|
||||
);
|
||||
}
|
||||
if complete {
|
||||
// SAFETY: request valid and not forwarded.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
||||
}
|
||||
}
|
||||
|
||||
// Copy `src` into the request's output memory and set the completed byte count.
|
||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; mem receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: mem valid; outlen receives the buffer size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: request valid.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
||||
};
|
||||
STATUS_SUCCESS
|
||||
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x}", status as u32);
|
||||
request.complete(status);
|
||||
}
|
||||
|
||||
// The 0x02 gate: a game writing an output report (rumble / lightbar / ADAPTIVE TRIGGERS). Per the
|
||||
// UMDF marshalling convention the report data is the *input* buffer and the report id is carried in
|
||||
// the *output* buffer length. We log it.
|
||||
fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
||||
// the *output* buffer length. We log it, then publish it to the DATA section for the host.
|
||||
fn on_output_report(request: &Request, ioctl: ULONG) -> NTSTATUS {
|
||||
let (bytes, inlen) = match request.input_bytes(64) {
|
||||
Ok(v) => v,
|
||||
Err(st) => return st,
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut inlen: usize = 0;
|
||||
// SAFETY: inmem valid.
|
||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
||||
as *const u8;
|
||||
let report_id = request.output_buffer_len() as u32; // report id, UMDF convention
|
||||
|
||||
// report id from output-buffer length (UMDF convention).
|
||||
let mut report_id: u32 = 0;
|
||||
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; output memory is optional here.
|
||||
if nt_success(unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut outmem)
|
||||
}) {
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: outmem valid.
|
||||
let _ =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
||||
report_id = outlen as u32;
|
||||
}
|
||||
|
||||
let n = inlen.min(48);
|
||||
let mut hex = String::new();
|
||||
if !inbuf.is_null() {
|
||||
// SAFETY: inbuf valid for inlen bytes; we read at most n.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(inbuf, n) };
|
||||
for b in bytes {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
for b in bytes.iter().take(48) {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
let kind = if ioctl == IOCTL_HID_WRITE_REPORT {
|
||||
"WRITE_REPORT"
|
||||
@@ -600,45 +504,29 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
|
||||
};
|
||||
dbglog!("[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
|
||||
|
||||
// Publish the game's 0x02 output report to shared memory for the host (rumble / lightbar /
|
||||
// player-LEDs / adaptive triggers). output_report @76, output_seq @72.
|
||||
if !inbuf.is_null() && inlen > 0 {
|
||||
let n = inlen.min(64);
|
||||
with_shm(|view| {
|
||||
// SAFETY: view is a mapped 256-byte section; write the report then bump the host-polled seq.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(inbuf, view.add(76), n);
|
||||
let seqp = view.add(72) as *mut u32;
|
||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
||||
core::ptr::write_unaligned(seqp, seq);
|
||||
}
|
||||
});
|
||||
// Publish the game's 0x02 output report to the sealed DATA section for the host (rumble /
|
||||
// lightbar / player-LEDs / adaptive triggers), then bump the host-polled output seq.
|
||||
if !bytes.is_empty()
|
||||
&& let Some(view) = CHANNEL.data()
|
||||
{
|
||||
view.write_bytes(OFF_OUTPUT, &bytes);
|
||||
let seq = view.read_u32(OFF_OUT_SEQ).wrapping_add(1);
|
||||
view.write_u32(OFF_OUT_SEQ, seq);
|
||||
}
|
||||
|
||||
// SAFETY: request valid.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, inlen as u64) };
|
||||
request.set_information(inlen as u64);
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense feature blob.
|
||||
fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
||||
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense/DualShock 4 blob.
|
||||
fn on_get_feature(request: &Request) -> NTSTATUS {
|
||||
let (bytes, _) = match request.input_bytes(1) {
|
||||
Ok(v) => v,
|
||||
Err(st) => return st,
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut inlen: usize = 0;
|
||||
// SAFETY: inmem valid.
|
||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
||||
as *const u8;
|
||||
if inbuf.is_null() || inlen < 1 {
|
||||
let Some(&report_id) = bytes.first() else {
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
// SAFETY: inbuf valid for >=1 byte.
|
||||
let report_id = unsafe { *inbuf };
|
||||
};
|
||||
// DualSense uses feature ids 0x05/0x09/0x20; DualShock 4 uses 0x02/0x12/0xa3.
|
||||
let blob: &[u8] = match (device_type() == 1, report_id) {
|
||||
(false, 0x05) => &DS_FEATURE_CALIBRATION,
|
||||
@@ -652,31 +540,21 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
};
|
||||
copy_to_output(request, blob)
|
||||
request.copy_to_output(blob)
|
||||
}
|
||||
|
||||
// IOCTL_HID_GET_STRING: the input is a ULONG whose low word is the string id and whose high word is
|
||||
// the language id. Reply with the requested device string as a NUL-terminated UTF-16 buffer. Native
|
||||
// PS5 / Steam code reads these (HidD_GetProductString / HidD_GetSerialNumberString — the serial is one
|
||||
// way they tell USB from BT); the old default returned STATUS_NOT_IMPLEMENTED, leaving them blank.
|
||||
// Observed live on this device, Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409) cyclically — the
|
||||
// manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; we map both forms.
|
||||
fn on_get_string(request: WDFREQUEST) -> NTSTATUS {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
||||
// way they tell USB from BT). Observed live: Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409)
|
||||
// cyclically — the manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; both.
|
||||
fn on_get_string(request: &Request) -> NTSTATUS {
|
||||
let (bytes, _) = match request.input_bytes(4) {
|
||||
Ok(v) => v,
|
||||
Err(st) => return st,
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut inlen: usize = 0;
|
||||
// SAFETY: inmem valid.
|
||||
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
|
||||
as *const u8;
|
||||
// SAFETY: inbuf is valid for inlen bytes; read the 4-byte id value when present.
|
||||
let id_val: u32 = if !inbuf.is_null() && inlen >= 4 {
|
||||
unsafe { core::ptr::read_unaligned(inbuf as *const u32) }
|
||||
let id_val: u32 = if bytes.len() >= 4 {
|
||||
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -706,96 +584,81 @@ fn on_get_string(request: WDFREQUEST) -> NTSTATUS {
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut wide: Vec<u16> = s.encode_utf16().collect();
|
||||
wide.push(0); // NUL terminator
|
||||
// SAFETY: reinterpret the UTF-16 buffer as bytes for the byte-oriented copy_to_output.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2) };
|
||||
copy_to_output(request, bytes)
|
||||
let mut wide: Vec<u8> = Vec::with_capacity(s.len() * 2 + 2);
|
||||
for u in s.encode_utf16() {
|
||||
wide.extend_from_slice(&u.to_le_bytes());
|
||||
}
|
||||
wide.extend_from_slice(&[0, 0]); // NUL terminator (UTF-16)
|
||||
request.copy_to_output(&wide)
|
||||
}
|
||||
|
||||
// Open + map the host's shared-memory section (Global\pfds-shm-0) and run `f` against the mapped base
|
||||
// if it exists with a valid magic, then unmap. NOT cached: re-mapped per access so the driver always
|
||||
// sees the current section (UMDF groups all devices in one WUDFHost, and the host may recreate the
|
||||
// section across restarts — a cached view would go stale). ~125 maps/s from the timer = negligible.
|
||||
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
|
||||
let name: Vec<u16> = format!("Global\\pfds-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: h is a valid mapping handle; map the whole section. The view keeps the section alive,
|
||||
// so the handle can be closed right away.
|
||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
||||
unsafe { CloseHandle(h) };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: view points at >= 4 mapped bytes.
|
||||
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
|
||||
if magic == SHM_MAGIC {
|
||||
if !LOGGED_SHM.swap(true, Ordering::Relaxed) {
|
||||
dbglog!(
|
||||
"[pf-ds] control: shared memory mapped (Global\\pfds-shm-{})",
|
||||
SHM_INDEX.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
f(view);
|
||||
}
|
||||
// SAFETY: view came from MapViewOfFile.
|
||||
unsafe { UnmapViewOfFile(view as *const c_void) };
|
||||
}
|
||||
|
||||
/// The host's device-type selector from shared memory (`device_type` byte @140): 0 = DualSense
|
||||
/// (default), 1 = DualShock 4. Read fresh on each enumeration query — cheap, and the host stamps the
|
||||
/// section before `SwDeviceCreate`, so it's set by the time hidclass asks for the descriptor /
|
||||
/// attributes. Defaults to DualSense if the section isn't mapped yet (magic absent).
|
||||
/// The host's device-type selector from the sealed DATA section (`device_type` @140): 0 = DualSense
|
||||
/// (default), 1 = DualShock 4. Read fresh on each enumeration query — cheap. If the channel hasn't
|
||||
/// attached when hidclass first asks (the host stamps the section + eager-delivers before
|
||||
/// `SwDeviceCreate` returns, but the handshake can be a few ms behind), pump the channel briefly —
|
||||
/// ONCE — for the delivery: a DS4 pad must not enumerate with the default DualSense identity because
|
||||
/// of a lost race. After that one bounded wait, fall back to the last observed type.
|
||||
fn device_type() -> u8 {
|
||||
let mut t = 0u8;
|
||||
with_shm(|view| {
|
||||
// SAFETY: view points at a mapped 256-byte section; the device-type byte is at offset 140.
|
||||
t = unsafe { *view.add(140) };
|
||||
});
|
||||
t
|
||||
if let Some(view) = CHANNEL.data() {
|
||||
let t = view.read_u8(OFF_DEVICE_TYPE);
|
||||
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
|
||||
return t;
|
||||
}
|
||||
if !DEVTYPE_WAITED.swap(true, Ordering::SeqCst) {
|
||||
let cfg = channel_cfg();
|
||||
for _ in 0..100 {
|
||||
if let Some(view) = CHANNEL.pump(&cfg) {
|
||||
let t = view.read_u8(OFF_DEVICE_TYPE);
|
||||
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
|
||||
return t;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
dbglog!(
|
||||
"[pf-ds] device_type: sealed channel not attached within 1s — defaulting to the last observed identity"
|
||||
);
|
||||
}
|
||||
LAST_DEVTYPE.load(Ordering::Relaxed) as u8
|
||||
}
|
||||
|
||||
extern "C" fn evt_timer(timer: WDFTIMER) {
|
||||
// Pull the latest host input report from shared memory (if the host has connected).
|
||||
with_shm(|view| {
|
||||
let mut buf = [0u8; 64];
|
||||
// SAFETY: view points at a mapped 256-byte section; input lives at offset 8..72.
|
||||
unsafe { core::ptr::copy_nonoverlapping(view.add(8), buf.as_mut_ptr(), 64) };
|
||||
if buf[0] == 0x01 {
|
||||
if let Ok(mut g) = INPUT_REPORT.lock() {
|
||||
// One sealed-channel tick: publish our pid / adopt a delivery / detect host-gone, then pull the
|
||||
// latest host input report from the attached DATA section (all safe, via pf_umdf_util).
|
||||
match CHANNEL.pump(&channel_cfg()) {
|
||||
Some(view) => {
|
||||
let mut buf = [0u8; 64];
|
||||
view.read_bytes(OFF_INPUT, &mut buf);
|
||||
if buf[0] == 0x01
|
||||
&& let Ok(mut g) = INPUT_REPORT.lock()
|
||||
{
|
||||
*g = buf;
|
||||
}
|
||||
// Health marks the host watches: driver_proto (attach signal, idempotent) and
|
||||
// driver_heartbeat (+1 per ~8 ms tick = liveness). Lets the host tell "driver bound
|
||||
// and alive" apart from "driver package missing/failed to bind".
|
||||
view.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
|
||||
let hb = view.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
|
||||
view.write_u32(OFF_DRIVER_HEARTBEAT, hb);
|
||||
}
|
||||
// Health marks the host watches: driver_proto @144 (attach signal, idempotent) and
|
||||
// driver_heartbeat @148 (+1 per ~8 ms tick = liveness). Lets the host tell "driver bound
|
||||
// and alive" apart from "driver package missing/failed to bind".
|
||||
// SAFETY: view points at a mapped 256-byte section; proto @144, heartbeat @148.
|
||||
unsafe {
|
||||
core::ptr::write_unaligned(view.add(144) as *mut u32, GAMEPAD_PROTO_VERSION);
|
||||
let hb = view.add(148) as *mut u32;
|
||||
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
|
||||
None => {
|
||||
// Host gone (mailbox name vanished) or channel not attached yet: feed games the neutral
|
||||
// report instead of a frozen last state (matters for the persistent out-of-band devnode,
|
||||
// which outlives host sessions).
|
||||
if let Ok(mut g) = INPUT_REPORT.lock() {
|
||||
*g = neutral_report(LAST_DEVTYPE.load(Ordering::Relaxed) == 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
// SAFETY: timer valid; parent is the manual queue.
|
||||
}
|
||||
|
||||
// Complete the next pended READ_REPORT with the current input report (safe queue/request API).
|
||||
// SAFETY: the timer's parent object is the manual queue (set in EvtDeviceAdd); the framework
|
||||
// guarantees a live handle here.
|
||||
let queue =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfTimerGetParentObject, timer) } as WDFQUEUE;
|
||||
let mut request: WDFREQUEST = core::ptr::null_mut();
|
||||
// SAFETY: queue valid; request receives the next pended request if any.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
||||
};
|
||||
if nt_success(st) {
|
||||
// SAFETY: `queue` is that live manual queue — the exact contract `retrieve_next_request` needs.
|
||||
if let Some(request) = unsafe { wdf::retrieve_next_request(queue) } {
|
||||
let report = INPUT_REPORT.lock().map(|g| *g).unwrap_or(NEUTRAL_REPORT);
|
||||
let s = copy_to_output(request, &report);
|
||||
// SAFETY: request valid and dequeued.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, s) };
|
||||
let st = request.copy_to_output(&report);
|
||||
request.complete(st);
|
||||
}
|
||||
let _ = STATUS_UNSUCCESSFUL; // keep the const referenced
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# pf-umdf-util - the audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers.
|
||||
# Everything a pad driver does with raw pointers or Win32/WDF FFI lives HERE, behind small safe
|
||||
# (or explicitly-contracted unsafe) APIs, so the driver crates' business logic is 100% safe Rust:
|
||||
# section - MappedView: bounds+alignment-checked shared-memory access (atomics for sync fields)
|
||||
# channel - ChannelClient: the sealed pad channel's driver-side state machine (a SAFE module)
|
||||
# wdf - Request/queue/device-property helpers over call_unsafe_wdf_function_binding
|
||||
[package]
|
||||
name = "pf-umdf-util"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
description = "punktfunk UMDF driver util: safe shared-memory + sealed-channel + WDF request primitives"
|
||||
|
||||
[dependencies]
|
||||
wdk-sys.workspace = true
|
||||
pf-driver-proto.workspace = true
|
||||
@@ -0,0 +1,192 @@
|
||||
//! The sealed pad channel, driver side (`design/gamepad-channel-sealing.md`, gamepad proto v2):
|
||||
//! poll the named bootstrap mailbox by index, publish our pid (iff the host's proto version
|
||||
//! matches), adopt the host-delivered DATA-section handle, and validate the mapped section's magic
|
||||
//! and `pad_index` before use. One implementation shared by `pf-xusb` and `pf-dualsense` (they used
|
||||
//! to hand-duplicate it), parameterized by [`ChannelConfig`].
|
||||
//!
|
||||
//! This module **forbids `unsafe`**: the entire state machine is safe Rust over
|
||||
//! [`section`](crate::section)'s checked accessors — the memory-safety surface of the sealed
|
||||
//! channel lives in that module alone.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::section::{MappedView, ViewCell, close_handle_value};
|
||||
use core::mem::offset_of;
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use pf_driver_proto::gamepad::{BOOT_MAGIC, GAMEPAD_PROTO_VERSION, PadBootstrap};
|
||||
|
||||
// PadBootstrap field offsets (the mailbox handshake; pinned by pf_driver_proto's asserts).
|
||||
const BOOT_OFF_MAGIC: usize = offset_of!(PadBootstrap, magic);
|
||||
const BOOT_OFF_HOST_PROTO: usize = offset_of!(PadBootstrap, host_proto);
|
||||
const BOOT_OFF_DRIVER_PID: usize = offset_of!(PadBootstrap, driver_pid);
|
||||
const BOOT_OFF_DRIVER_PROTO: usize = offset_of!(PadBootstrap, driver_proto);
|
||||
const BOOT_OFF_DATA_HANDLE: usize = offset_of!(PadBootstrap, data_handle);
|
||||
const BOOT_OFF_HANDLE_PID: usize = offset_of!(PadBootstrap, handle_pid);
|
||||
const BOOT_OFF_HANDLE_SEQ: usize = offset_of!(PadBootstrap, handle_seq);
|
||||
const BOOT_SIZE: usize = core::mem::size_of::<PadBootstrap>();
|
||||
|
||||
/// What varies between the two pad drivers.
|
||||
pub struct ChannelConfig {
|
||||
/// Log-line prefix (`"pf-xusb"` / `"pf-ds"`).
|
||||
pub tag: &'static str,
|
||||
/// Mailbox name prefix, completed with the pad index (`"Global\\pfxusb-boot-"` / `"Global\\pfds-boot-"`).
|
||||
pub boot_name_prefix: &'static str,
|
||||
/// The DATA section's magic (`XUSB_MAGIC` / `PAD_MAGIC`).
|
||||
pub data_magic: u32,
|
||||
/// The DATA section's size (`size_of::<XusbShm>()` / `size_of::<PadShm>()`).
|
||||
pub data_size: usize,
|
||||
/// `offset_of!(…Shm, pad_index)` in the DATA section.
|
||||
pub pad_index_off: usize,
|
||||
/// The driver's logger (each driver tees to its own debug file).
|
||||
pub log: fn(&str),
|
||||
}
|
||||
|
||||
/// Per-pad channel state (a `static` in each driver — per-pad because
|
||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` gives each pad its own WUDFHost).
|
||||
pub struct ChannelClient {
|
||||
/// The pad index from the devnode Location (which mailbox to poll + the `pad_index` the
|
||||
/// delivered DATA section must carry).
|
||||
index: AtomicU32,
|
||||
/// The adopted DATA view; leaked-on-publish (see [`ViewCell`]) so a re-delivery can never
|
||||
/// unmap a view a concurrent callback still reads through.
|
||||
data: ViewCell,
|
||||
/// The last `handle_seq` consumed (CAS-guarded so concurrent pumps adopt a delivery exactly
|
||||
/// once). Reset to 0 when the mailbox disappears, so a NEW host session's delivery is always
|
||||
/// fresh even if its (per-host-process) seq counter collides with the previous session's.
|
||||
consumed_seq: AtomicU32,
|
||||
logged_proto_mismatch: AtomicBool,
|
||||
logged_pid: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for ChannelClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
pub const fn new() -> ChannelClient {
|
||||
ChannelClient {
|
||||
index: AtomicU32::new(0),
|
||||
data: ViewCell::new(),
|
||||
consumed_seq: AtomicU32::new(0),
|
||||
logged_proto_mismatch: AtomicBool::new(false),
|
||||
logged_pid: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the pad index (from the devnode Location, in `EvtDeviceAdd`).
|
||||
pub fn set_index(&self, idx: u32) {
|
||||
self.index.store(idx, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The adopted DATA view regardless of mailbox liveness — for write paths where acting on a
|
||||
/// stale section is harmless (the pump owns the detach semantics).
|
||||
pub fn data(&self) -> Option<&'static MappedView> {
|
||||
self.data.get()
|
||||
}
|
||||
|
||||
/// One tick of the sealed-channel state machine: publish our pid (+ proto version) in the
|
||||
/// mailbox, adopt a delivered DATA handle, and return the attached DATA view — `None` while
|
||||
/// unattached, on a host/driver version mismatch (fail closed), or when the mailbox is gone
|
||||
/// (host gone). The mailbox is re-opened by name on every call: the name existing doubles as
|
||||
/// host-liveness (the host closes it when the pad is torn down).
|
||||
pub fn pump(&self, cfg: &ChannelConfig) -> Option<&'static MappedView> {
|
||||
let name = format!("{}{}", cfg.boot_name_prefix, self.index());
|
||||
let boot = match MappedView::open_named(&name, BOOT_SIZE) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
// Mailbox gone → the host (or this pad) is gone. Forget the consumed seq so the
|
||||
// NEXT host session's first delivery always reads as fresh.
|
||||
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Acquire pairs with the host's Release magic store, so a valid magic implies `host_proto`
|
||||
// is visible. A missing/garbled magic reads as "no usable mailbox" (same as absent).
|
||||
if boot.load_u32(BOOT_OFF_MAGIC, Ordering::Acquire) != BOOT_MAGIC {
|
||||
self.consumed_seq.store(0, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
// Publish our proto version first (idempotent) — the host logs a mismatch even when we
|
||||
// refuse to publish a pid below.
|
||||
boot.store_u32(
|
||||
BOOT_OFF_DRIVER_PROTO,
|
||||
GAMEPAD_PROTO_VERSION,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
let host_proto = boot.load_u32(BOOT_OFF_HOST_PROTO, Ordering::Relaxed);
|
||||
if host_proto != GAMEPAD_PROTO_VERSION {
|
||||
if !self.logged_proto_mismatch.swap(true, Ordering::Relaxed) {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] host proto {host_proto} != driver proto {GAMEPAD_PROTO_VERSION} — \
|
||||
refusing the handshake (update host + drivers together)",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
return None; // version mismatch — fail closed
|
||||
}
|
||||
let mypid = std::process::id();
|
||||
if boot.load_u32(BOOT_OFF_DRIVER_PID, Ordering::Relaxed) != mypid {
|
||||
boot.store_u32(BOOT_OFF_DRIVER_PID, mypid, Ordering::Release);
|
||||
if !self.logged_pid.swap(true, Ordering::Relaxed) {
|
||||
(cfg.log)(&format!("[{}] bootstrap: published pid {mypid}", cfg.tag));
|
||||
}
|
||||
}
|
||||
// A delivery addressed to us we haven't consumed? CAS so concurrent pumps (worker thread /
|
||||
// timer + IOCTL paths) adopt exactly once.
|
||||
let seq = boot.load_u32(BOOT_OFF_HANDLE_SEQ, Ordering::Acquire);
|
||||
let cur = self.consumed_seq.load(Ordering::Relaxed);
|
||||
if seq != 0
|
||||
&& seq != cur
|
||||
&& boot.load_u32(BOOT_OFF_HANDLE_PID, Ordering::Relaxed) == mypid
|
||||
&& self
|
||||
.consumed_seq
|
||||
.compare_exchange(cur, seq, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
self.adopt(cfg, boot.load_u64(BOOT_OFF_DATA_HANDLE, Ordering::Relaxed));
|
||||
}
|
||||
self.data()
|
||||
}
|
||||
|
||||
/// Map + validate a delivered DATA-section handle VALUE (untrusted until the mapped section
|
||||
/// carries our magic AND our pad index). On success we own the handle (adopt-on-success) and
|
||||
/// close it — the view keeps the section alive. On validation failure the handle is
|
||||
/// deliberately NOT closed: a tampered value could name an unrelated handle in our own table.
|
||||
fn adopt(&self, cfg: &ChannelConfig, value: u64) {
|
||||
let Some(view) = MappedView::from_handle_value(value, cfg.data_size) else {
|
||||
if value != 0 {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] delivered DATA handle 0x{value:x} did not map — ignoring",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
return;
|
||||
};
|
||||
let magic = view.load_u32(0, Ordering::Relaxed);
|
||||
let idx = view.load_u32(cfg.pad_index_off, Ordering::Relaxed);
|
||||
let want = self.index();
|
||||
if magic != cfg.data_magic || idx != want {
|
||||
(cfg.log)(&format!(
|
||||
"[{}] delivered DATA section failed validation (magic 0x{magic:08x}, pad_index \
|
||||
{idx}, want {want}) — ignoring",
|
||||
cfg.tag
|
||||
));
|
||||
// `view` drops here → unmapped; the handle stays open (see above).
|
||||
return;
|
||||
}
|
||||
// The value resolved to OUR pad's section, so it is the handle the host duplicated for us —
|
||||
// we own it; the (about-to-be-leaked) view keeps the section alive after the close.
|
||||
close_handle_value(value);
|
||||
self.data.set(view);
|
||||
(cfg.log)(&format!(
|
||||
"[{}] sealed pad channel mapped (index {want})",
|
||||
cfg.tag
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! The audited unsafe-primitive layer under the punktfunk UMDF gamepad drivers (`pf-xusb`,
|
||||
//! `pf-dualsense`).
|
||||
//!
|
||||
//! A UMDF driver cannot be literally free of `unsafe` — WDF dispatch, Win32 section mapping and
|
||||
//! cross-process shared memory are FFI by nature. What Rust *can* buy is confining every raw
|
||||
//! operation to one small, reviewed layer with explicit contracts, so the drivers' business logic
|
||||
//! (the sealed-channel state machine, report plumbing, IOCTL policy) is **100 % safe code** and a
|
||||
//! memory-safety bug can only live in this crate. Three modules:
|
||||
//!
|
||||
//! * [`section`] — [`section::MappedView`]: bounds- and alignment-checked access to a mapped shared
|
||||
//! section (atomics for the cross-process sync fields), plus the leaked-view [`section::ViewCell`].
|
||||
//! * [`channel`] — [`channel::ChannelClient`]: the sealed pad channel's driver side
|
||||
//! (`design/gamepad-channel-sealing.md`), a **`#[forbid(unsafe_code)]` module** — the entire
|
||||
//! handshake/validation/adoption state machine is safe Rust over [`section`]'s API.
|
||||
//! * [`wdf`] — [`wdf::Request`] + queue/device-property helpers: each framework callback converts
|
||||
//! its raw `WDFREQUEST` into a token exactly once (`unsafe`, with the framework's validity as the
|
||||
//! contract); everything after that is safe.
|
||||
//!
|
||||
//! Lint gates (mirrored in every driver crate, enforced by the drivers CI clippy step):
|
||||
//! `unsafe_op_in_unsafe_fn` + `clippy::undocumented_unsafe_blocks` — every remaining `unsafe {}`
|
||||
//! must carry a `// SAFETY:` proof.
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
pub mod channel;
|
||||
pub mod section;
|
||||
pub mod wdf;
|
||||
|
||||
/// `NT_SUCCESS` — an NTSTATUS is an error iff negative.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn nt_success(status: wdk_sys::NTSTATUS) -> bool {
|
||||
status >= 0
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Safe access to Win32 shared-memory sections: [`MappedView`] wraps a mapped view of a known
|
||||
//! length and exposes bounds- and alignment-checked accessors, so callers never touch the raw base
|
||||
//! pointer. Cross-process sync fields (seqs, pids, handle values) go through real atomics; bulk
|
||||
//! report regions use plain unaligned copies, guarded by the channel protocol's seq fields — the
|
||||
//! same access discipline the host side uses (`inject/windows/gamepad_raii.rs`).
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering};
|
||||
|
||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
|
||||
|
||||
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
|
||||
unsafe extern "system" {
|
||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||
fn CloseHandle(h: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
/// A read/write view over a mapped shared section of exactly `len` bytes. Every accessor
|
||||
/// bounds-checks (and, for the atomic ones, alignment-checks) its offset, so no caller can read or
|
||||
/// write outside the mapping — the offsets are `offset_of!` constants from `pf_driver_proto`, making
|
||||
/// a failed check a compile-shaped logic bug (it aborts the WUDFHost rather than corrupting).
|
||||
///
|
||||
/// Concurrency: the peer process writes the section concurrently. Fields used for cross-process
|
||||
/// synchronization must be accessed through the `load_*`/`store_*` atomic accessors; the bulk
|
||||
/// byte/scalar accessors are plain unaligned accesses whose consistency is guarded by the channel
|
||||
/// protocol (seq-fenced publishes), exactly as on the host side.
|
||||
pub struct MappedView {
|
||||
base: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
// SAFETY: `MappedView` is a pointer + length over an OS mapping that stays valid until
|
||||
// `UnmapViewOfFile` in `Drop` (or forever, once leaked into a `ViewCell`). All access goes through
|
||||
// the checked accessors — atomics for shared sync fields, unaligned reads/writes for bulk data —
|
||||
// none of which require a single-thread owner, so sharing/sending the view across the driver's
|
||||
// callback threads is sound.
|
||||
unsafe impl Send for MappedView {}
|
||||
// SAFETY: as above — `&MappedView` only exposes accessors that are safe under concurrent use.
|
||||
unsafe impl Sync for MappedView {}
|
||||
|
||||
impl MappedView {
|
||||
/// Open the named section `name` and map its first `len` bytes read/write. `None` if the name
|
||||
/// does not exist (e.g. the host is gone) or the mapping fails. The section handle is closed
|
||||
/// immediately — the view keeps the section alive.
|
||||
pub fn open_named(name: &str, len: usize) -> Option<MappedView> {
|
||||
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
// SAFETY: `wide` is a valid NUL-terminated UTF-16 string for the duration of the call.
|
||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, wide.as_ptr()) };
|
||||
if h.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `h` is the valid mapping handle just opened; map `len` bytes read/write. The view
|
||||
// keeps the section alive, so the handle can be closed right away.
|
||||
let base = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, len) } as *mut u8;
|
||||
// SAFETY: `h` is the valid handle from `OpenFileMappingW`, owned solely by this function.
|
||||
unsafe { CloseHandle(h) };
|
||||
if base.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(MappedView { base, len })
|
||||
}
|
||||
|
||||
/// Map `len` bytes of a section from a raw handle VALUE (the sealed channel's delivery — a
|
||||
/// handle the host duplicated into this process). `None` if the value does not resolve to a
|
||||
/// mappable section. The handle itself is NOT consumed — the caller decides after validating
|
||||
/// the mapped content (see [`close_handle_value`]).
|
||||
pub fn from_handle_value(value: u64, len: usize) -> Option<MappedView> {
|
||||
if value == 0 {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `MapViewOfFile` on an arbitrary handle value is safe — it fails (returns null)
|
||||
// unless the value resolves to a section handle in this process's table with RW access.
|
||||
let base = unsafe { MapViewOfFile(value as usize as *mut c_void, FILE_MAP_RW, 0, 0, len) }
|
||||
as *mut u8;
|
||||
if base.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(MappedView { base, len })
|
||||
}
|
||||
|
||||
/// Assert `off..off+n` is inside the view and, for atomics, `align`-aligned. The view base is
|
||||
/// page-aligned (`MapViewOfFile`), so field alignment reduces to offset alignment.
|
||||
#[inline]
|
||||
fn check(&self, off: usize, n: usize, align: usize) {
|
||||
assert!(
|
||||
off.is_multiple_of(align) && off.checked_add(n).is_some_and(|end| end <= self.len),
|
||||
"MappedView access out of bounds/alignment (off={off}, n={n}, len={})",
|
||||
self.len
|
||||
);
|
||||
}
|
||||
|
||||
/// Atomic `u32` load at `off` (must be 4-aligned) — the cross-process sync accessor.
|
||||
#[inline]
|
||||
pub fn load_u32(&self, off: usize, order: Ordering) -> u32 {
|
||||
self.check(off, 4, 4);
|
||||
// SAFETY: `off` is in-bounds + 4-aligned per `check`, and the page-aligned mapping stays
|
||||
// valid while `&self` lives; an `AtomicU32` view over shared memory is the defined way to
|
||||
// race the peer process.
|
||||
unsafe { (*(self.base.add(off) as *const AtomicU32)).load(order) }
|
||||
}
|
||||
|
||||
/// Atomic `u32` store at `off` (must be 4-aligned).
|
||||
#[inline]
|
||||
pub fn store_u32(&self, off: usize, v: u32, order: Ordering) {
|
||||
self.check(off, 4, 4);
|
||||
// SAFETY: as `load_u32` — in-bounds, aligned, valid for `&self`'s lifetime.
|
||||
unsafe { (*(self.base.add(off) as *const AtomicU32)).store(v, order) }
|
||||
}
|
||||
|
||||
/// Atomic `u64` load at `off` (must be 8-aligned).
|
||||
#[inline]
|
||||
pub fn load_u64(&self, off: usize, order: Ordering) -> u64 {
|
||||
self.check(off, 8, 8);
|
||||
// SAFETY: as `load_u32`, with 8-byte size/alignment checked.
|
||||
unsafe { (*(self.base.add(off) as *const AtomicU64)).load(order) }
|
||||
}
|
||||
|
||||
/// Plain byte read at `off` (bulk-region accessor — protocol-guarded, see the type docs).
|
||||
#[inline]
|
||||
pub fn read_u8(&self, off: usize) -> u8 {
|
||||
self.check(off, 1, 1);
|
||||
// SAFETY: in-bounds per `check`; a one-byte read cannot tear.
|
||||
unsafe { *self.base.add(off) }
|
||||
}
|
||||
|
||||
/// Plain byte write at `off`.
|
||||
#[inline]
|
||||
pub fn write_u8(&self, off: usize, v: u8) {
|
||||
self.check(off, 1, 1);
|
||||
// SAFETY: in-bounds per `check`; a one-byte write cannot tear.
|
||||
unsafe { *self.base.add(off) = v }
|
||||
}
|
||||
|
||||
/// Plain (unaligned) `u16` read at `off`.
|
||||
#[inline]
|
||||
pub fn read_u16(&self, off: usize) -> u16 {
|
||||
self.check(off, 2, 1);
|
||||
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u16) }
|
||||
}
|
||||
|
||||
/// Plain (unaligned) `u32` read at `off` — the bulk-region accessor for a DATA-section scalar
|
||||
/// (host-written state / a driver-written publish counter; consistency comes from the channel
|
||||
/// protocol's seq fences, not from this access, exactly as on the host side).
|
||||
#[inline]
|
||||
pub fn read_u32(&self, off: usize) -> u32 {
|
||||
self.check(off, 4, 1);
|
||||
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const u32) }
|
||||
}
|
||||
|
||||
/// Plain (unaligned) `u32` write at `off` (bulk-region accessor).
|
||||
#[inline]
|
||||
pub fn write_u32(&self, off: usize, v: u32) {
|
||||
self.check(off, 4, 1);
|
||||
// SAFETY: in-bounds per `check`; `write_unaligned` has no alignment requirement.
|
||||
unsafe { core::ptr::write_unaligned(self.base.add(off) as *mut u32, v) }
|
||||
}
|
||||
|
||||
/// Plain (unaligned) `i16` read at `off`.
|
||||
#[inline]
|
||||
pub fn read_i16(&self, off: usize) -> i16 {
|
||||
self.check(off, 2, 1);
|
||||
// SAFETY: in-bounds per `check`; `read_unaligned` has no alignment requirement.
|
||||
unsafe { core::ptr::read_unaligned(self.base.add(off) as *const i16) }
|
||||
}
|
||||
|
||||
/// Copy `dst.len()` bytes out of the view starting at `off`.
|
||||
pub fn read_bytes(&self, off: usize, dst: &mut [u8]) {
|
||||
self.check(off, dst.len(), 1);
|
||||
// SAFETY: the source range is in-bounds per `check`; `dst` is a live exclusive borrow of
|
||||
// `dst.len()` writable bytes and cannot overlap the foreign mapping.
|
||||
unsafe { core::ptr::copy_nonoverlapping(self.base.add(off), dst.as_mut_ptr(), dst.len()) }
|
||||
}
|
||||
|
||||
/// Copy `src` into the view starting at `off`.
|
||||
pub fn write_bytes(&self, off: usize, src: &[u8]) {
|
||||
self.check(off, src.len(), 1);
|
||||
// SAFETY: the destination range is in-bounds per `check`; `src` is a live borrow that
|
||||
// cannot overlap the foreign mapping.
|
||||
unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), self.base.add(off), src.len()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MappedView {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `base` is the live view from `MapViewOfFile`, unmapped exactly once (here).
|
||||
unsafe {
|
||||
UnmapViewOfFile(self.base as *const c_void);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Close a raw handle VALUE owned by this process — the sealed channel's adopt-on-success step
|
||||
/// (the mapped view keeps the section alive after the close). Closing a value that is not a live
|
||||
/// handle of this process is a logic error the OS rejects (returns FALSE); it is not memory-unsafe.
|
||||
pub fn close_handle_value(value: u64) {
|
||||
if value == 0 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: `CloseHandle` validates the value against this process's handle table; no memory is
|
||||
// dereferenced through it.
|
||||
unsafe { CloseHandle(value as usize as *mut c_void) };
|
||||
}
|
||||
|
||||
/// A lock-free cell holding the driver's adopted DATA view as a **leaked** `&'static MappedView`.
|
||||
/// [`set`](Self::set) leaks the new view (and abandons the old one) instead of ever unmapping:
|
||||
/// a concurrent framework callback may still be reading through a previously-returned reference, so
|
||||
/// the mapping must never be torn down — a deliberate, bounded leak (one small view per delivery,
|
||||
/// at most a handful per pad lifetime).
|
||||
pub struct ViewCell(AtomicPtr<MappedView>);
|
||||
|
||||
impl Default for ViewCell {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewCell {
|
||||
pub const fn new() -> ViewCell {
|
||||
ViewCell(AtomicPtr::new(core::ptr::null_mut()))
|
||||
}
|
||||
|
||||
/// The current view, if one was published. The `'static` lifetime is real: published views are
|
||||
/// leaked and never unmapped.
|
||||
pub fn get(&self) -> Option<&'static MappedView> {
|
||||
let p = self.0.load(Ordering::Acquire);
|
||||
// SAFETY: `p` is either null or a `Box::leak`ed `MappedView` published by `set`, which is
|
||||
// never dropped or unmapped — so the reference is valid for the process lifetime.
|
||||
(!p.is_null()).then(|| unsafe { &*p })
|
||||
}
|
||||
|
||||
/// Publish `view`, leaking it (and abandoning — NOT freeing — any previous view; see the type
|
||||
/// docs for why the old mapping must stay alive).
|
||||
pub fn set(&self, view: MappedView) {
|
||||
let leaked: &'static mut MappedView = Box::leak(Box::new(view));
|
||||
self.0.swap(leaked, Ordering::Release);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! Safe(ly-contracted) helpers over the WDF request/memory/property DDIs the pad drivers use. The
|
||||
//! pattern: a framework callback converts its raw `WDFREQUEST` into a [`Request`] token **once**
|
||||
//! (`unsafe`, the framework's validity guarantee is the contract); every operation after that is a
|
||||
//! safe method, and completion consumes the token so a request cannot be completed twice or used
|
||||
//! after completion from safe code.
|
||||
|
||||
use wdk_sys::{
|
||||
NTSTATUS, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
||||
call_unsafe_wdf_function_binding,
|
||||
};
|
||||
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
||||
/// wdk_sys root; the value is stable WDM).
|
||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
|
||||
/// A validity token for one framework-delivered `WDFREQUEST`. Not `Copy`/`Clone`: completing or
|
||||
/// forwarding consumes it, so safe code cannot touch a request the framework already owns again.
|
||||
pub struct Request(WDFREQUEST);
|
||||
|
||||
impl Request {
|
||||
/// Wrap the raw request handed to the current framework callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `raw` must be the live, framework-provided `WDFREQUEST` of the callback invocation this is
|
||||
/// called from (WDF owns handle validity; a forged/dangling handle is framework UB).
|
||||
pub unsafe fn new(raw: WDFREQUEST) -> Request {
|
||||
Request(raw)
|
||||
}
|
||||
|
||||
/// Complete the request with `status` (consumes the token — the framework owns it afterwards).
|
||||
pub fn complete(self, status: NTSTATUS) {
|
||||
// SAFETY: `self.0` is the live callback request per `Request::new`'s contract, not yet
|
||||
// completed or forwarded (both consume the token).
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, self.0, status) };
|
||||
}
|
||||
|
||||
/// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
||||
/// Returns the status to complete with (`STATUS_INVALID_BUFFER_SIZE` if the buffer is short).
|
||||
pub fn copy_to_output(&self, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; `mem` receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: `mem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: `mem` is valid and at least `src.len()` bytes; `src` is a live borrow.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut core::ffi::c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: `self.0` is the live callback request.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, src.len() as u64)
|
||||
};
|
||||
0 // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
/// The request's input buffer: up to `cap` bytes copied out, plus the buffer's TRUE length.
|
||||
/// `Err(status)` if the input memory can't be retrieved (propagate as the completion status).
|
||||
pub fn input_bytes(&self, cap: usize) -> Result<(Vec<u8>, usize), NTSTATUS> {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; `inmem` receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, self.0, &mut inmem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return Err(st);
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: `inmem` is the valid memory object just retrieved; `len` receives its size.
|
||||
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
||||
as *const u8;
|
||||
if p.is_null() {
|
||||
return Ok((Vec::new(), 0));
|
||||
}
|
||||
let n = len.min(cap);
|
||||
// SAFETY: `p` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `n <= len`.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(p, n) }.to_vec();
|
||||
Ok((bytes, len))
|
||||
}
|
||||
|
||||
/// The request's output-buffer LENGTH (0 if unavailable) — UMDF HID marshalling carries the
|
||||
/// output-report id in it.
|
||||
pub fn output_buffer_len(&self) -> usize {
|
||||
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `self.0` is the live callback request; output memory is optional here.
|
||||
if !nt_success(unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, self.0, &mut outmem)
|
||||
}) {
|
||||
return 0;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: `outmem` is the valid memory object just retrieved; `outlen` receives its size.
|
||||
let _ =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
||||
outlen
|
||||
}
|
||||
|
||||
/// Set the completed-bytes information field (for paths that complete with a length but no
|
||||
/// output copy, e.g. echoing an output report's length).
|
||||
pub fn set_information(&self, info: u64) {
|
||||
// SAFETY: `self.0` is the live callback request.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, self.0, info) };
|
||||
}
|
||||
|
||||
/// Forward the request to a manual queue. On success the framework owns it (the token is
|
||||
/// consumed by value — the caller cannot touch the request again); on failure the token is
|
||||
/// handed back with the status so the caller completes it. (`Request` has no `Drop`, so the
|
||||
/// consumed-on-success token simply falls out of scope — nothing to run.)
|
||||
///
|
||||
/// # Safety
|
||||
/// `queue` must be a live manual `WDFQUEUE` of the same device (e.g. the one created in
|
||||
/// `EvtDeviceAdd` and stashed in a static).
|
||||
pub unsafe fn forward_to_queue(self, queue: WDFQUEUE) -> Result<(), (Request, NTSTATUS)> {
|
||||
// SAFETY: `self.0` is the live callback request; `queue` is live per this fn's contract.
|
||||
let st =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, self.0, queue) };
|
||||
if nt_success(st) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((self, st))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop the next pended request off a manual queue (`None` when empty).
|
||||
///
|
||||
/// # Safety
|
||||
/// `queue` must be a live manual `WDFQUEUE` (e.g. the timer's parent object).
|
||||
pub unsafe fn retrieve_next_request(queue: WDFQUEUE) -> Option<Request> {
|
||||
let mut request: WDFREQUEST = core::ptr::null_mut();
|
||||
// SAFETY: `queue` is live per this fn's contract; `request` receives the next pended request.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
||||
};
|
||||
// SAFETY: on success `request` is a live framework request this caller now services — the
|
||||
// exact contract `Request::new` requires.
|
||||
nt_success(st).then(|| unsafe { Request::new(request) })
|
||||
}
|
||||
|
||||
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), a
|
||||
/// NUL-terminated UTF-16 decimal string. Defaults to 0 (single-pad) if absent. (The WDFMEMORY is
|
||||
/// device-parented and freed by the framework at device teardown — one small alloc per device add.)
|
||||
///
|
||||
/// # Safety
|
||||
/// `device` must be the live `WDFDEVICE` created in the current `EvtDeviceAdd`.
|
||||
pub unsafe fn query_location_index(device: WDFDEVICE) -> u32 {
|
||||
let mut mem: wdk_sys::WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: `device` is live per this fn's contract; property = LocationInformation; pool ignored
|
||||
// in UMDF; `mem` receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceAllocAndQueryProperty,
|
||||
device,
|
||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
||||
0,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut mem
|
||||
)
|
||||
};
|
||||
if !nt_success(st) || mem.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: `mem` is the valid memory object just allocated; `len` receives its size.
|
||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
||||
as *const u16;
|
||||
if buf.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let units = (len / 2).min(8);
|
||||
// SAFETY: `buf` is valid for `len` bytes per `WdfMemoryGetBuffer`; we read `units * 2 <= len`.
|
||||
let chars = unsafe { core::slice::from_raw_parts(buf, units) };
|
||||
let mut idx: u32 = 0;
|
||||
let mut any = false;
|
||||
for &c in chars {
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
if (0x30..=0x39).contains(&c) {
|
||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any { idx } else { 0 }
|
||||
}
|
||||
@@ -42,8 +42,10 @@ AddReg=pf_vdisplay_HardwareDeviceSettings
|
||||
[pf_vdisplay_HardwareDeviceSettings]
|
||||
HKR, , "UpperFilters", %REG_MULTI_SZ%, "IndirectKmd"
|
||||
HKR, "WUDF", "DeviceGroupId", %REG_SZ%, "pfVDisplayGroup"
|
||||
; Let the host (LocalSystem service) + admins open the control device for the ADD/REMOVE/PING IOCTLs.
|
||||
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;WD)"
|
||||
; Only the host (LocalSystem service) + admins may open the control device. Deliberately NO Everyone
|
||||
; ACE (SudoVDA ships one for its user-mode host): the control plane creates/removes monitors and
|
||||
; bootstraps the sealed frame channel (IOCTL_SET_FRAME_CHANNEL), so it is not for unprivileged callers.
|
||||
HKR, , "Security", , "D:P(A;;GA;;;SY)(A;;GA;;;BA)"
|
||||
|
||||
[pf_vdisplay_Install.NT.Services]
|
||||
AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
|
||||
|
||||
@@ -36,6 +36,8 @@ struct SendAdapter(iddcx::IDDCX_ADAPTER);
|
||||
// SAFETY: an opaque IddCx handle, used only as an argument to IddCx DDIs (themselves the synchronisation
|
||||
// point) — never dereferenced in Rust. Storing it across threads in a OnceLock is sound.
|
||||
unsafe impl Send for SendAdapter {}
|
||||
// SAFETY: as above — the handle is only ever passed by value to IddCx DDIs, never dereferenced, so
|
||||
// shared `&SendAdapter` access across threads is sound.
|
||||
unsafe impl Sync for SendAdapter {}
|
||||
|
||||
static ADAPTER: OnceLock<SendAdapter> = OnceLock::new();
|
||||
|
||||
@@ -51,8 +51,9 @@ pub unsafe extern "C" fn parse_monitor_description(
|
||||
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION,
|
||||
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
||||
) -> NTSTATUS {
|
||||
// SAFETY: framework-provided in/out args, valid for the call.
|
||||
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||
let in_args = unsafe { &*p_in };
|
||||
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||
let out_args = unsafe { &mut *p_out };
|
||||
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
||||
let edid = unsafe {
|
||||
@@ -100,8 +101,9 @@ pub unsafe extern "C" fn parse_monitor_description2(
|
||||
p_in: *const iddcx::IDARG_IN_PARSEMONITORDESCRIPTION2,
|
||||
p_out: *mut iddcx::IDARG_OUT_PARSEMONITORDESCRIPTION,
|
||||
) -> NTSTATUS {
|
||||
// SAFETY: framework-provided in/out args, valid for the call.
|
||||
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||
let in_args = unsafe { &*p_in };
|
||||
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||
let out_args = unsafe { &mut *p_out };
|
||||
// SAFETY: the framework supplies a valid EDID buffer of `DataSize` bytes.
|
||||
let edid = unsafe {
|
||||
@@ -156,8 +158,9 @@ pub unsafe extern "C" fn monitor_query_modes(
|
||||
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES,
|
||||
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
||||
) -> NTSTATUS {
|
||||
// SAFETY: framework-provided in/out args, valid for the call.
|
||||
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||
let in_args = unsafe { &*p_in };
|
||||
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||
let out_args = unsafe { &mut *p_out };
|
||||
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
||||
return STATUS_NOT_FOUND;
|
||||
@@ -183,8 +186,9 @@ pub unsafe extern "C" fn monitor_query_modes2(
|
||||
p_in: *const iddcx::IDARG_IN_QUERYTARGETMODES2,
|
||||
p_out: *mut iddcx::IDARG_OUT_QUERYTARGETMODES,
|
||||
) -> NTSTATUS {
|
||||
// SAFETY: framework-provided in/out args, valid for the call.
|
||||
// SAFETY: the framework supplies a valid, live input-args pointer for the call.
|
||||
let in_args = unsafe { &*p_in };
|
||||
// SAFETY: the framework supplies a valid, live output-args pointer for the call.
|
||||
let out_args = unsafe { &mut *p_out };
|
||||
let Some(modes) = crate::monitor::modes_for_object(monitor) else {
|
||||
return STATUS_NOT_FOUND;
|
||||
@@ -279,7 +283,8 @@ pub unsafe extern "C" fn assign_swap_chain(
|
||||
drop(crate::monitor::take_swap_chain_processor(monitor));
|
||||
|
||||
// The OS target id (stamped on the monitor at creation, after IddCxMonitorArrival) keys the
|
||||
// per-monitor objects STEP 6's host opens. 0 (default) if the monitor isn't found.
|
||||
// frame-channel stash STEP 6's worker attaches from (the host addresses its IOCTL_SET_FRAME_CHANNEL
|
||||
// delivery by this id). 0 (default) if the monitor isn't found — the worker then never attaches.
|
||||
let target_id = crate::monitor::target_id_for_object(monitor).unwrap_or(0);
|
||||
|
||||
if let Some(device) = crate::direct_3d_device::pooled_device(luid) {
|
||||
|
||||
@@ -93,6 +93,8 @@ pub unsafe fn dispatch(request: WDFREQUEST, ioctl_code: u32) {
|
||||
}
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
control::IOCTL_SET_RENDER_ADAPTER => unsafe { set_render_adapter(request) },
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
control::IOCTL_SET_FRAME_CHANNEL => unsafe { set_frame_channel(request) },
|
||||
_ => complete(request, STATUS_NOT_FOUND),
|
||||
}
|
||||
}
|
||||
@@ -148,11 +150,49 @@ unsafe fn add(request: WDFREQUEST) {
|
||||
adapter_luid_high: luid_high,
|
||||
target_id,
|
||||
resolved_monitor_id: monitor_id,
|
||||
// This WUDFHost's pid — where the host duplicates the sealed frame channel's handles INTO
|
||||
// (`ProcessSharingDisabled`: this process is exclusively ours and dies with the device).
|
||||
wudf_pid: std::process::id(),
|
||||
};
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
unsafe { write_output_complete(request, &reply) };
|
||||
}
|
||||
|
||||
/// `IOCTL_SET_FRAME_CHANNEL`: adopt the handle values the host duplicated into this process and stash
|
||||
/// them on the target monitor for the swap-chain worker to attach with. The ownership contract with
|
||||
/// the host is **adopt-on-success only**: this driver owns (and eventually closes) the handles iff the
|
||||
/// IOCTL completes successfully; on ANY error completion it leaves them untouched, because the host
|
||||
/// reaps its remote duplicates whenever the IOCTL fails — a close on both sides would double-close
|
||||
/// values the OS may already have reused for unrelated handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `request` is the framework `WDFREQUEST`.
|
||||
unsafe fn set_frame_channel(request: WDFREQUEST) {
|
||||
// SAFETY: `request` is the framework WDFREQUEST.
|
||||
let Some(req) = (unsafe { read_input::<control::SetFrameChannelRequest>(request) }) else {
|
||||
complete(request, STATUS_INVALID_PARAMETER);
|
||||
return;
|
||||
};
|
||||
// A malformed request adopts nothing (no FrameChannel is built, so no Drop can close anything).
|
||||
let Some(ch) = crate::frame_transport::FrameChannel::from_request(&req) else {
|
||||
complete(request, STATUS_INVALID_PARAMETER);
|
||||
return;
|
||||
};
|
||||
match crate::monitor::set_frame_channel(req.target_id, ch) {
|
||||
Ok(()) => complete(request, STATUS_SUCCESS),
|
||||
Err(ch) => {
|
||||
dbglog!(
|
||||
"[pf-vd] SET_FRAME_CHANNEL: no monitor with target_id {} — rejecting (host reaps the handles)",
|
||||
req.target_id
|
||||
);
|
||||
// NOT adopted: disarm the channel so its Drop does NOT close the handles (see the contract
|
||||
// above — the host's error path reaps them remotely).
|
||||
ch.into_unowned();
|
||||
complete(request, STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `IOCTL_REMOVE`: depart + drop the monitor for the given session id.
|
||||
///
|
||||
/// # Safety
|
||||
|
||||
@@ -123,10 +123,10 @@ static DEVICE_POOL: Mutex<Option<(i64, Arc<Direct3DDevice>)>> = Mutex::new(None)
|
||||
pub fn pooled_device(luid: LUID) -> Option<Arc<Direct3DDevice>> {
|
||||
let key = (i64::from(luid.HighPart) << 32) | i64::from(luid.LowPart);
|
||||
let mut pool = DEVICE_POOL.lock().ok()?;
|
||||
if let Some((k, dev)) = pool.as_ref() {
|
||||
if *k == key {
|
||||
return Some(dev.clone());
|
||||
}
|
||||
if let Some((k, dev)) = pool.as_ref()
|
||||
&& *k == key
|
||||
{
|
||||
return Some(dev.clone());
|
||||
}
|
||||
match Direct3DDevice::init(luid) {
|
||||
Ok(d) => {
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
//! STEP 6 — IDD-push frame publisher (DRIVER side).
|
||||
//! STEP 6 — IDD-push frame publisher (DRIVER side), attached over the **sealed channel**.
|
||||
//!
|
||||
//! The restricted WUDFHost token canNOT create named kernel objects (proven on the RTX box: it can't
|
||||
//! even write a world-writable file), so — exactly like the gamepad UMDF drivers
|
||||
//! (`crates/punktfunk-host/src/inject/dualsense_windows.rs`: *"the host creates the section, privileged,
|
||||
//! with a permissive SDDL so the WUDFHost can open it; the driver maps it"*) — the **host** creates the
|
||||
//! shared header + frame-ready event + ring of keyed-mutex textures, and the driver only **OPENS** them.
|
||||
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header (our
|
||||
//! only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write files),
|
||||
//! then copies each acquired swap-chain surface into the next ring slot and signals the host.
|
||||
//! The restricted WUDFHost token canNOT create named kernel objects — and since the frame channel
|
||||
//! carries whole-desktop pixels, the objects are not merely host-created but **unnamed**: nothing to
|
||||
//! enumerate, open by name, or pre-create ("squat"). The **host** creates the shared header +
|
||||
//! frame-ready event + ring of keyed-mutex textures with no names, duplicates the handles INTO this
|
||||
//! WUDFHost process (`DuplicateHandle` — SYSTEM can, we can't reciprocate, which is why the host is the
|
||||
//! broker), and delivers the handle VALUES over `IOCTL_SET_FRAME_CHANNEL` ([`crate::control`] stashes
|
||||
//! them per monitor as a [`FrameChannel`]). The swap-chain worker picks the stash up and attaches with
|
||||
//! [`FramePublisher::from_channel`]. Only the two endpoint processes ever hold a handle to any frame
|
||||
//! object — see `design/idd-push-security.md`.
|
||||
//!
|
||||
//! Host counterpart: `crates/punktfunk-host/src/capture/idd_push.rs`. The shared `SharedHeader` layout,
|
||||
//! the [`FrameToken`] packing, the `Global\` object-name scheme, the `MAGIC`/`RING_LEN` and the
|
||||
//! `DRV_STATUS_*` codes are NOT hand-duplicated here: both sides `use pf_driver_proto::frame::*`, which
|
||||
//! OWNS the contract (with `const` size asserts so any drift is a compile error).
|
||||
//! The driver writes its actual render-adapter LUID + a status code back into the host-created header
|
||||
//! (our only driver-visibility channel: UMDF hides OutputDebugString in ETW and the token can't write
|
||||
//! files), then copies each acquired swap-chain surface into the next ring slot and signals the host.
|
||||
//!
|
||||
//! Ported from the proven oracle (`packaging/windows/vdisplay-driver/pf-vdisplay/src/frame_transport.rs`).
|
||||
//! Differences from the oracle:
|
||||
//! * the layout/consts/names/token come from `pf_driver_proto::frame` instead of being re-declared;
|
||||
//! * `dbglog!` replaces `log::info!`;
|
||||
//! * the optional fixed-name `Global\pfvd-dbg` `DebugBlock` bring-up channel is SKIPPED (not on the data
|
||||
//! path). FOLLOW-UP: if the host bring-up diagnostics are needed again, port the oracle's `DebugBlock`
|
||||
//! here too (it is owned by `idd_push.rs`, not the proto).
|
||||
//! Host counterpart: `crates/punktfunk-host/src/capture/windows/idd_push.rs`. The shared `SharedHeader`
|
||||
//! layout, the [`FrameToken`] packing, the `MAGIC`/`RING_LEN`, the `DRV_STATUS_*` codes and the
|
||||
//! channel-delivery struct are NOT hand-duplicated here: both sides `use pf_driver_proto::{control,
|
||||
//! frame}`, which OWNS the contract (with `const` size asserts so any drift is a compile error).
|
||||
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
|
||||
use pf_driver_proto::control::SetFrameChannelRequest;
|
||||
use pf_driver_proto::frame::{
|
||||
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, FrameToken, MAGIC, RING_LEN,
|
||||
SharedHeader, event_name, header_name, texture_name,
|
||||
SharedHeader,
|
||||
};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
@@ -34,28 +32,95 @@ use windows::Win32::Graphics::Direct3D11::{
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::IDXGIKeyedMutex;
|
||||
use windows::Win32::System::Memory::{
|
||||
FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW,
|
||||
UnmapViewOfFile,
|
||||
FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile,
|
||||
};
|
||||
use windows::Win32::System::Threading::{OpenEventW, SYNCHRONIZATION_ACCESS_RIGHTS, SetEvent};
|
||||
use windows::core::{HSTRING, Interface};
|
||||
use windows::Win32::System::Threading::SetEvent;
|
||||
use windows::core::Interface;
|
||||
|
||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` — passed to `OpenSharedResourceByName` (matches the host's
|
||||
/// `CreateSharedHandle` access). Kept local: it is a `OpenSharedResourceByName` arg, not part of the
|
||||
/// proto contract. (Same value the host uses in `idd_push.rs`.)
|
||||
const DXGI_SHARED_RESOURCE_RW: u32 = 0x8000_0000 | 0x1;
|
||||
/// SYNCHRONIZE | EVENT_MODIFY_STATE — the driver does not wait on the event, only SIGNALS it.
|
||||
const EVENT_ACCESS: u32 = 0x0010_0000 | 0x0002;
|
||||
/// `WAIT_TIMEOUT` as an HRESULT — `AcquireSync` returns this when the slot is held by the consumer.
|
||||
const WAIT_TIMEOUT_HRESULT: i32 = 0x0000_0102;
|
||||
|
||||
/// One monitor's sealed-channel bootstrap: the handle VALUES the host duplicated into THIS process
|
||||
/// (`IOCTL_SET_FRAME_CHANNEL`). Owning a `FrameChannel` means owning those handles — exactly one of
|
||||
/// {the monitor stash ([`crate::monitor`]), a [`FramePublisher`] under construction} holds it at any
|
||||
/// time, and `Drop` closes every entry not consumed, so a replaced/unmatched/failed delivery can never
|
||||
/// leak entries in the WUDFHost handle table. A `0` field means "taken" (or never valid) and is skipped.
|
||||
pub struct FrameChannel {
|
||||
/// The ring generation these textures belong to (checked against the header at attach).
|
||||
generation: u32,
|
||||
ring_len: u32,
|
||||
header: u64,
|
||||
event: u64,
|
||||
textures: [u64; RING_LEN as usize],
|
||||
}
|
||||
|
||||
impl FrameChannel {
|
||||
/// Validate + adopt the handle values from the host's IOCTL. `None` on a malformed request (bad
|
||||
/// `ring_len`, zero handles) — the caller completes with `STATUS_INVALID_PARAMETER` and nothing is
|
||||
/// adopted (a zero value is never treated as a handle).
|
||||
pub fn from_request(req: &SetFrameChannelRequest) -> Option<Self> {
|
||||
if req.ring_len == 0 || req.ring_len > RING_LEN {
|
||||
return None;
|
||||
}
|
||||
if req.header_handle == 0
|
||||
|| req.event_handle == 0
|
||||
|| req.texture_handles[..req.ring_len as usize].contains(&0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
generation: req.generation,
|
||||
ring_len: req.ring_len,
|
||||
header: req.header_handle,
|
||||
event: req.event_handle,
|
||||
textures: req.texture_handles,
|
||||
})
|
||||
}
|
||||
|
||||
/// Move a handle value out of the channel: the caller now owns it; `Drop` skips the zeroed slot.
|
||||
fn take(v: &mut u64) -> HANDLE {
|
||||
HANDLE(core::mem::take(v) as usize as *mut core::ffi::c_void)
|
||||
}
|
||||
|
||||
/// Disarm without closing anything — for the adopt-on-success-only contract: a delivery rejected
|
||||
/// with an error completion was never adopted, and the HOST reaps its remote duplicates on that
|
||||
/// error, so closing here too would double-close (see `crate::control::set_frame_channel`).
|
||||
pub fn into_unowned(mut self) {
|
||||
self.header = 0;
|
||||
self.event = 0;
|
||||
self.textures = [0; RING_LEN as usize];
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameChannel {
|
||||
fn drop(&mut self) {
|
||||
for v in [&mut self.header, &mut self.event]
|
||||
.into_iter()
|
||||
.chain(self.textures.iter_mut())
|
||||
{
|
||||
if *v != 0 {
|
||||
let h = Self::take(v);
|
||||
// SAFETY: `h` is a live handle the host duplicated into this process for us to own; it
|
||||
// was not consumed (non-zero), so this is its sole close.
|
||||
unsafe {
|
||||
let _ = CloseHandle(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB: `FrameChannel` is plain integers, so it is auto-`Send` — it crosses from the control-plane
|
||||
// dispatch thread (stash) to the swap-chain worker (attach) with `MONITOR_MODES` serializing the
|
||||
// hand-off; no manual impl needed (handle values are process-global tokens, not thread-affine).
|
||||
|
||||
struct Slot {
|
||||
tex: ID3D11Texture2D,
|
||||
mutex: IDXGIKeyedMutex,
|
||||
}
|
||||
|
||||
/// Publishes acquired swap-chain surfaces into the HOST-created ring. Owned by the swap-chain processor
|
||||
/// thread; attached lazily once the host has created the shared objects.
|
||||
/// thread; attached lazily once the host's channel delivery lands in the monitor stash.
|
||||
pub struct FramePublisher {
|
||||
context: ID3D11DeviceContext,
|
||||
map: HANDLE,
|
||||
@@ -70,7 +135,8 @@ pub struct FramePublisher {
|
||||
ring_format: u32,
|
||||
/// The ring generation this publisher attached to. The host BUMPS the header generation when it
|
||||
/// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`]
|
||||
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
|
||||
/// detects that so `run_core` re-attaches to the new ring (whose channel the host re-delivers)
|
||||
/// instead of dropping every frame.
|
||||
generation: u32,
|
||||
/// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a
|
||||
/// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1).
|
||||
@@ -81,102 +147,99 @@ pub struct FramePublisher {
|
||||
unsafe impl Send for FramePublisher {}
|
||||
|
||||
impl FramePublisher {
|
||||
/// Try ONCE to attach to the host-created shared objects. Returns `Err` cheaply if the host hasn't
|
||||
/// created/published them yet — the drain loop retries periodically, so a non-IDD-push session just
|
||||
/// keeps draining with no stall. All early-return paths clean up the handles/mapping they opened
|
||||
/// explicitly (raw-handle style, no RAII — matches the rest of this driver).
|
||||
pub fn try_open(
|
||||
target_id: u32,
|
||||
/// Attach to the host ring from a delivered [`FrameChannel`]. Consumes the channel: on ANY failure
|
||||
/// every handle is closed (taken ones explicitly, the rest by the channel's `Drop`) and the host
|
||||
/// re-delivers on the next recreate — there is nothing to poll, so failure is terminal for THIS
|
||||
/// delivery (the host's `wait_for_attach` sees the status code and fails the session open). All
|
||||
/// early-return paths clean up explicitly (raw-handle style, no RAII — matches the rest of this
|
||||
/// driver).
|
||||
pub fn from_channel(
|
||||
mut channel: FrameChannel,
|
||||
render_luid_low: u32,
|
||||
render_luid_high: i32,
|
||||
device: &ID3D11Device,
|
||||
context: &ID3D11DeviceContext,
|
||||
) -> windows::core::Result<Self> {
|
||||
// 1. Open the host-created header (RW). Err if the host hasn't created it yet.
|
||||
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call (`?` returns on failure).
|
||||
let map = unsafe {
|
||||
OpenFileMappingW(
|
||||
FILE_MAP_ALL_ACCESS.0,
|
||||
false,
|
||||
&HSTRING::from(header_name(target_id)),
|
||||
)?
|
||||
};
|
||||
// SAFETY: `map` is the just-opened file mapping; mapping size_of::<SharedHeader>() bytes of it
|
||||
// (the host created the mapping at >= that size). The null `view.Value` is checked below.
|
||||
let ring_len = channel.ring_len;
|
||||
|
||||
// 1. Map the header from the duplicated section handle (ours from here on).
|
||||
let map = FrameChannel::take(&mut channel.header);
|
||||
// SAFETY: `map` is the live section handle the host duplicated into this process; mapping
|
||||
// size_of::<SharedHeader>() bytes of it (the host created the mapping at >= that size). The null
|
||||
// `view.Value` is checked below.
|
||||
let view = unsafe {
|
||||
// Read/write only — the host now duplicates the header handle with least access
|
||||
// (`SECTION_MAP_READ | SECTION_MAP_WRITE`), so `FILE_MAP_ALL_ACCESS` would exceed the
|
||||
// granted rights and fail. We read the layout + write status/publish-token fields; RW covers it.
|
||||
MapViewOfFile(
|
||||
map,
|
||||
FILE_MAP_ALL_ACCESS,
|
||||
FILE_MAP_READ | FILE_MAP_WRITE,
|
||||
0,
|
||||
0,
|
||||
core::mem::size_of::<SharedHeader>(),
|
||||
)
|
||||
};
|
||||
if view.Value.is_null() {
|
||||
// SAFETY: `map` is the just-opened mapping handle, closed once here on the error path.
|
||||
let err = windows::core::Error::from_win32();
|
||||
// SAFETY: `map` is the taken section handle, closed once here on the error path (the rest of
|
||||
// `channel` closes via its Drop).
|
||||
unsafe {
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(windows::core::Error::from_win32());
|
||||
return Err(err);
|
||||
}
|
||||
let header = view.Value.cast::<SharedHeader>();
|
||||
|
||||
// 2. Report our render adapter to the host immediately (lets it detect a mismatch).
|
||||
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>() bytes);
|
||||
// these scalar writes are within it. The host opened the section with a permissive SDDL for us.
|
||||
// SAFETY: `header` points to the mapped, non-null host header (>= size_of::<SharedHeader>()
|
||||
// bytes); these scalar writes are within it.
|
||||
unsafe {
|
||||
(*header).driver_render_luid_low = render_luid_low;
|
||||
(*header).driver_render_luid_high = render_luid_high;
|
||||
}
|
||||
|
||||
// 3. The host sets magic==MAGIC only once the ring textures exist. Not ready → retry later.
|
||||
// SAFETY: `header` is the mapped host header; `magic` lives within it and is read atomically
|
||||
// (Acquire) to pair with the host's Release store once the ring textures are published.
|
||||
let magic = unsafe {
|
||||
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32)).load(Ordering::Acquire)
|
||||
// 3. The host stamps magic==MAGIC BEFORE delivering the channel, and this channel's generation
|
||||
// must match the header's CURRENT generation — a mismatch means the host recreated the ring
|
||||
// again before we attached (a fresh delivery is on its way); drop this stale one.
|
||||
// SAFETY: `header` is the mapped host header; `magic`/`generation` live within it and are read
|
||||
// atomically (Acquire) to pair with the host's Release publishes.
|
||||
let (magic, header_gen) = unsafe {
|
||||
(
|
||||
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32))
|
||||
.load(Ordering::Acquire),
|
||||
(*(core::ptr::addr_of!((*header).generation) as *const AtomicU32))
|
||||
.load(Ordering::Acquire),
|
||||
)
|
||||
};
|
||||
if magic != MAGIC {
|
||||
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once on this path.
|
||||
if magic != MAGIC || header_gen != channel.generation {
|
||||
dbglog!(
|
||||
"[pf-vd] frame-push(driver): dropping channel delivery (magic ok: {}, channel gen {} vs header gen {header_gen})",
|
||||
magic == MAGIC,
|
||||
channel.generation
|
||||
);
|
||||
// SAFETY: `header`/`map` are the live mapped view + taken handle; unmapped + closed once on
|
||||
// this path.
|
||||
unsafe {
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: header.cast(),
|
||||
});
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(windows::core::Error::from_win32());
|
||||
// E_BOUNDS — stand-in for "stale delivery"; the caller only drops the attempt.
|
||||
return Err(windows::core::HRESULT(0x8000_000Bu32 as i32).into());
|
||||
}
|
||||
// SAFETY: `header` is the mapped host header; these scalar fields live within it.
|
||||
let (generation, ring_len) =
|
||||
unsafe { ((*header).generation, (*header).ring_len.min(RING_LEN)) };
|
||||
|
||||
// 4. Open the event (SYNCHRONIZE | EVENT_MODIFY_STATE so we can SetEvent).
|
||||
// SAFETY: a plain Win32 call; the name HSTRING is valid for the call.
|
||||
let event = match unsafe {
|
||||
OpenEventW(
|
||||
SYNCHRONIZATION_ACCESS_RIGHTS(EVENT_ACCESS),
|
||||
false,
|
||||
&HSTRING::from(event_name(target_id)),
|
||||
)
|
||||
} {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
// SAFETY: `header`/`map` are the live mapped view + handle; unmapped + closed once here.
|
||||
unsafe {
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: header.cast(),
|
||||
});
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// 4. The frame-ready event (duplicated with the host handle's full access, so SetEvent works).
|
||||
let event = FrameChannel::take(&mut channel.event);
|
||||
|
||||
// 5. Open device1 + the ring textures the host created (same render adapter required).
|
||||
// 5. Open device1 + the ring textures from their duplicated shared handles (same render adapter
|
||||
// required). Each NT handle is closed right after the open — the COM object holds its own
|
||||
// reference, and the HOST keeps the resource alive with its own handle.
|
||||
let device1: ID3D11Device1 = match device.cast() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are the
|
||||
// live handles, all released once on this error path.
|
||||
// SAFETY: `header` is the mapped host header (status write within it); `event`/`map` are
|
||||
// the taken live handles, all released once on this error path.
|
||||
unsafe {
|
||||
(*header).driver_status = DRV_STATUS_NO_DEVICE1;
|
||||
let _ = CloseHandle(event);
|
||||
@@ -189,45 +252,45 @@ impl FramePublisher {
|
||||
}
|
||||
};
|
||||
let mut slots = Vec::new();
|
||||
for k in 0..ring_len {
|
||||
let name = HSTRING::from(texture_name(target_id, generation, k));
|
||||
// SAFETY: `device1` is a live ID3D11Device1; the name HSTRING is valid for the call.
|
||||
// Take each texture handle one at a time (NOT the whole array up front), so an error return
|
||||
// mid-loop still lets `channel`'s Drop close every not-yet-taken handle.
|
||||
for value in channel.textures.iter_mut().take(ring_len as usize) {
|
||||
let tex_handle = FrameChannel::take(value);
|
||||
// SAFETY: `device1` is a live ID3D11Device1; `tex_handle` is the duplicated shared NT handle
|
||||
// for this ring texture.
|
||||
let opened: windows::core::Result<ID3D11Texture2D> =
|
||||
unsafe { device1.OpenSharedResourceByName(&name, DXGI_SHARED_RESOURCE_RW) };
|
||||
match opened {
|
||||
unsafe { device1.OpenSharedResource1(tex_handle) };
|
||||
// SAFETY: `tex_handle` is ours (taken above) and no longer needed whether the open succeeded
|
||||
// (the COM object holds the resource) or failed — close it exactly once here.
|
||||
unsafe {
|
||||
let _ = CloseHandle(tex_handle);
|
||||
}
|
||||
let failed = match opened {
|
||||
Ok(tex) => match tex.cast::<IDXGIKeyedMutex>() {
|
||||
Ok(mutex) => slots.push(Slot { tex, mutex }),
|
||||
Err(e) => {
|
||||
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
||||
// are the live handles, all released once on this error path.
|
||||
unsafe {
|
||||
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
||||
(*header).driver_status_detail = e.code().0 as u32;
|
||||
let _ = CloseHandle(event);
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: header.cast(),
|
||||
});
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(e);
|
||||
Ok(mutex) => {
|
||||
slots.push(Slot { tex, mutex });
|
||||
None
|
||||
}
|
||||
Err(e) => Some(e),
|
||||
},
|
||||
Err(e) => {
|
||||
// Most likely a render-adapter mismatch (the host made the textures on a different
|
||||
// GPU than the swap-chain renders on). Tell the host so it can report it.
|
||||
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
||||
// are the live handles, all released once on this error path.
|
||||
unsafe {
|
||||
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
||||
(*header).driver_status_detail = e.code().0 as u32;
|
||||
let _ = CloseHandle(event);
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: header.cast(),
|
||||
});
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(e);
|
||||
// Most likely a render-adapter mismatch (the host made the textures on a different GPU
|
||||
// than the swap-chain renders on). Tell the host so it can report it.
|
||||
Err(e) => Some(e),
|
||||
};
|
||||
if let Some(e) = failed {
|
||||
// SAFETY: `header` is the mapped host header (status writes within it); `event`/`map`
|
||||
// are the taken live handles, all released once on this error path (the not-yet-taken
|
||||
// texture handles close via `channel`'s Drop).
|
||||
unsafe {
|
||||
(*header).driver_status = DRV_STATUS_TEX_FAIL;
|
||||
(*header).driver_status_detail = e.code().0 as u32;
|
||||
let _ = CloseHandle(event);
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
Value: header.cast(),
|
||||
});
|
||||
let _ = CloseHandle(map);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +299,7 @@ impl FramePublisher {
|
||||
(*header).driver_status = DRV_STATUS_OPENED;
|
||||
}
|
||||
dbglog!(
|
||||
"[pf-vd] frame-push(driver): attached to host ring gen {generation} ({ring_len} slots)"
|
||||
"[pf-vd] frame-push(driver): attached to host ring gen {header_gen} ({ring_len} slots, sealed channel)"
|
||||
);
|
||||
Ok(Self {
|
||||
context: context.clone(),
|
||||
@@ -248,7 +311,7 @@ impl FramePublisher {
|
||||
seq: 0,
|
||||
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
|
||||
ring_format: unsafe { (*header).dxgi_format },
|
||||
generation,
|
||||
generation: header_gen,
|
||||
mismatch_logged: false,
|
||||
})
|
||||
}
|
||||
@@ -261,8 +324,8 @@ impl FramePublisher {
|
||||
}
|
||||
|
||||
/// True once the host has recreated the ring (bumped the header generation) — e.g. the display's HDR
|
||||
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and the texture names now carry a new
|
||||
/// generation. `run_core` drops the publisher on this so it re-attaches to the new ring.
|
||||
/// mode flipped, so the ring format changed (FP16 ⇄ BGRA) and a fresh channel delivery is coming.
|
||||
/// `run_core` drops the publisher on this so it re-attaches to the new ring.
|
||||
pub fn is_stale(&self) -> bool {
|
||||
// SAFETY: `self.header` stays mapped for the publisher's lifetime; `generation` lives within it and
|
||||
// is read atomically (Acquire) to pair with the host's Release bump on a mid-session ring recreate.
|
||||
@@ -338,8 +401,8 @@ impl FramePublisher {
|
||||
}
|
||||
.pack();
|
||||
self.latest_cell().store(latest, Ordering::Release);
|
||||
// SAFETY: `self.event` is the live host-created frame-ready event we opened with
|
||||
// EVENT_MODIFY_STATE; signalling it wakes the host consumer.
|
||||
// SAFETY: `self.event` is the live host-created frame-ready event, duplicated into
|
||||
// this process with the creator's access; signalling it wakes the host consumer.
|
||||
unsafe {
|
||||
let _ = SetEvent(self.event);
|
||||
}
|
||||
@@ -357,10 +420,11 @@ impl FramePublisher {
|
||||
impl Drop for FramePublisher {
|
||||
fn drop(&mut self) {
|
||||
// Slots FIRST (release the shared textures + keyed mutexes), THEN unmap the header, THEN the
|
||||
// handles.
|
||||
// handles — nothing of the channel outlives the publisher (teardown invariant,
|
||||
// `design/idd-push-security.md`).
|
||||
self.slots.clear();
|
||||
// SAFETY: drop runs once; `self.header` (if non-null) is the live mapped view and `self.event`/
|
||||
// `self.map` are the live handles this publisher opened — each unmapped/closed exactly once here.
|
||||
// `self.map` are the live handles this publisher owns — each unmapped/closed exactly once here.
|
||||
unsafe {
|
||||
if !self.header.is_null() {
|
||||
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
|
||||
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
||||
// P0 lint (audit §8): an unsafe op inside an `unsafe fn` must be in an explicit `unsafe {}` block, so the
|
||||
// fn-level `unsafe` never silently blesses the whole body. (The per-site `// SAFETY:` discipline already
|
||||
// landed in STEP 8.)
|
||||
// fn-level `unsafe` never silently blesses the whole body, AND every `unsafe {}` must carry a `// SAFETY:`
|
||||
// proof. An IddCx display driver is inherently FFI-bound (D3D11 / IddCx DDIs / cross-process shared
|
||||
// textures), so it can't be unsafe-FREE the way the gamepad drivers now are (their logic moved onto the
|
||||
// safe `pf_umdf_util` layer); these gates make it unsafe-AUDITED instead, and stop it regressing.
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[macro_use]
|
||||
mod log;
|
||||
|
||||
@@ -45,11 +45,11 @@ pub fn log(s: &str) {
|
||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||
}
|
||||
use std::io::Write;
|
||||
if let Some(m) = file_appender() {
|
||||
if let Ok(mut f) = m.lock() {
|
||||
let _ = writeln!(f, "{s}");
|
||||
let _ = f.flush();
|
||||
}
|
||||
if let Some(m) = file_appender()
|
||||
&& let Ok(mut f) = m.lock()
|
||||
{
|
||||
let _ = writeln!(f, "{s}");
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ pub struct MonitorObject {
|
||||
/// The live swap-chain drain worker, set by `assign_swap_chain` and dropped (RAII-joins the worker
|
||||
/// thread) by `unassign_swap_chain` / departure (STEP 5).
|
||||
pub swap_chain_processor: Option<crate::swap_chain_processor::SwapChainProcessor>,
|
||||
/// The host's sealed-channel delivery (`IOCTL_SET_FRAME_CHANNEL`) awaiting pickup by the swap-chain
|
||||
/// worker ([`take_frame_channel`]). Exactly one owner per delivery: replacing or dropping the entry
|
||||
/// closes an unconsumed channel's handles via [`FrameChannel`]'s `Drop`, so no delivery can leak
|
||||
/// handles in the WUDFHost table whatever the monitor's fate.
|
||||
pub frame_channel: Option<crate::frame_transport::FrameChannel>,
|
||||
/// When the entry was created — the watchdog skips still-initializing monitors.
|
||||
pub created_at: Instant,
|
||||
}
|
||||
@@ -256,8 +261,8 @@ pub fn modes_for_object(object: iddcx::IDDCX_MONITOR) -> Option<Vec<Mode>> {
|
||||
.map(|m| m.modes.clone())
|
||||
}
|
||||
|
||||
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to name the
|
||||
/// shared-ring objects). `None` if the monitor isn't found.
|
||||
/// The OS target id stamped on the monitor whose handle matches (used by `assign_swap_chain` to key the
|
||||
/// frame-channel stash for its worker). `None` if the monitor isn't found.
|
||||
pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
|
||||
MONITOR_MODES
|
||||
.lock()
|
||||
@@ -267,6 +272,52 @@ pub fn target_id_for_object(object: iddcx::IDDCX_MONITOR) -> Option<u32> {
|
||||
.map(|m| m.target_id)
|
||||
}
|
||||
|
||||
/// Stash a host frame-channel delivery on the monitor with `target_id` (an ARRIVED monitor — a pending
|
||||
/// entry's `target_id` is still 0, which the host can never send since OS target ids are non-zero).
|
||||
/// Replacing an unconsumed delivery drops it → its handles close (it WAS adopted by a prior success).
|
||||
/// `Err(ch)` if no such monitor exists — the caller must NOT close those handles (the host only sees
|
||||
/// the error status and reaps its remote duplicates itself; closing here too would double-close values
|
||||
/// the OS may have reused).
|
||||
pub fn set_frame_channel(
|
||||
target_id: u32,
|
||||
ch: crate::frame_transport::FrameChannel,
|
||||
) -> Result<(), crate::frame_transport::FrameChannel> {
|
||||
if target_id == 0 {
|
||||
return Err(ch);
|
||||
}
|
||||
let mut lock = lock_monitors();
|
||||
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
|
||||
m.frame_channel = Some(ch);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take (remove) the pending frame-channel delivery for `target_id`, transferring handle ownership to
|
||||
/// the caller (the swap-chain worker's attach). `None` until the host delivers one.
|
||||
pub fn take_frame_channel(target_id: u32) -> Option<crate::frame_transport::FrameChannel> {
|
||||
if target_id == 0 {
|
||||
return None;
|
||||
}
|
||||
lock_monitors()
|
||||
.iter_mut()
|
||||
.find(|m| m.target_id == target_id)?
|
||||
.frame_channel
|
||||
.take()
|
||||
}
|
||||
|
||||
/// Is a frame-channel delivery pending for `target_id`? The swap-chain worker treats a pending
|
||||
/// delivery as NEWEST-WINS: it supersedes an attached publisher, because the host only re-delivers
|
||||
/// after (re)creating the ring — and a retry-created ring is a DIFFERENT header mapping, whose
|
||||
/// generation bump an old publisher (mapped to the previous header) can never observe.
|
||||
pub fn has_frame_channel(target_id: u32) -> bool {
|
||||
target_id != 0
|
||||
&& lock_monitors()
|
||||
.iter()
|
||||
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
|
||||
}
|
||||
|
||||
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
|
||||
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
|
||||
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
|
||||
@@ -351,6 +402,7 @@ pub fn create_monitor(
|
||||
adapter_luid_low: 0,
|
||||
adapter_luid_high: 0,
|
||||
swap_chain_processor: None,
|
||||
frame_channel: None,
|
||||
created_at: Instant::now(),
|
||||
});
|
||||
id
|
||||
|
||||
@@ -78,6 +78,8 @@ pub struct SwapChainProcessor {
|
||||
// SAFETY: Raw ptr is managed by external library; access is serialised by the worker thread + the
|
||||
// terminate flag.
|
||||
unsafe impl Send for SwapChainProcessor {}
|
||||
// SAFETY: as above — the raw pointer is only touched by the serialised worker, so a shared
|
||||
// `&SwapChainProcessor` reference exposes no unsynchronised access.
|
||||
unsafe impl Sync for SwapChainProcessor {}
|
||||
|
||||
impl SwapChainProcessor {
|
||||
@@ -223,10 +225,11 @@ impl SwapChainProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring. The restricted UMDF token can't
|
||||
// create named objects, so the host creates the header + event + textures and we only OPEN them
|
||||
// once they appear (`try_open`). Until then we just drain — exactly the STEP-5 behaviour — so a
|
||||
// non-IDD-push session never stalls. Retried every ~30 loop iterations.
|
||||
// STEP 6 IDD-push: lazily ATTACH to the HOST-created shared ring over the SEALED channel. The
|
||||
// frame objects are unnamed — the host duplicates their handles into this process and delivers
|
||||
// the values via IOCTL_SET_FRAME_CHANNEL, which the control plane stashes on our monitor
|
||||
// (`monitor::take_frame_channel`). Until a delivery lands we just drain — exactly the STEP-5
|
||||
// behaviour — so a non-IDD-push session never stalls. The stash is polled every ~30 iterations.
|
||||
let mut publisher: Option<FramePublisher> = None;
|
||||
let mut frames_since_try: u32 = u32::MAX; // attach attempt on the first loop iteration
|
||||
|
||||
@@ -243,31 +246,41 @@ impl SwapChainProcessor {
|
||||
break;
|
||||
}
|
||||
|
||||
// The host recreates the shared ring (new format) mid-session when the display's HDR mode
|
||||
// flips — it bumps the header generation. Detect that and drop the publisher so we re-attach to
|
||||
// the new-format textures below; otherwise we'd keep CopyResource'ing into the stale ring, whose
|
||||
// format now mismatches the surface → the publish() format-guard drops every frame and the
|
||||
// stream freezes until the next swap-chain recreate.
|
||||
if publisher.as_ref().is_some_and(FramePublisher::is_stale) {
|
||||
// Re-attach triggers, either of:
|
||||
// * `is_stale` — the host recreated the ring mid-session (HDR flip): it bumps OUR header's
|
||||
// generation and re-delivers; without dropping here we'd keep CopyResource'ing into the
|
||||
// stale ring, whose format now mismatches the surface → the publish() format-guard drops
|
||||
// every frame and the stream freezes until the next swap-chain recreate.
|
||||
// * a PENDING delivery (newest-wins) — a host build-retry creates a whole NEW ring with a
|
||||
// DIFFERENT header mapping; the old publisher's header never changes, so `is_stale` can't
|
||||
// fire. The host only delivers after fully (re)creating a ring, so a pending delivery
|
||||
// always supersedes whatever we're attached to.
|
||||
if publisher.as_ref().is_some_and(FramePublisher::is_stale)
|
||||
|| (publisher.is_some() && crate::monitor::has_frame_channel(target_id))
|
||||
{
|
||||
publisher = None;
|
||||
frames_since_try = u32::MAX; // re-attach immediately
|
||||
}
|
||||
// Lazy-attach (rate-limited) at the loop TOP so we keep trying even while the display is idle
|
||||
// (E_PENDING / no frames presented yet), not only when a frame is acquired. `try_open` is a
|
||||
// cheap OpenFileMapping that fails fast until the host has created the ring.
|
||||
// (E_PENDING / no frames presented yet), not only when a frame is acquired. Checking the
|
||||
// stash is a cheap mutex peek that stays empty until the host's channel delivery lands; a
|
||||
// taken delivery is consumed whether the attach succeeds or not (on failure its handles are
|
||||
// closed, the host's wait-for-attach reads the status code, and any retry is a NEW delivery).
|
||||
if publisher.is_none() {
|
||||
if frames_since_try >= 30 {
|
||||
frames_since_try = 0;
|
||||
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
|
||||
// happy under `-D warnings`; semantics are identical — attach on success, retry on Err.
|
||||
if let Ok(p) = FramePublisher::try_open(
|
||||
target_id,
|
||||
render_luid_low,
|
||||
render_luid_high,
|
||||
&device.device,
|
||||
&device.device_context,
|
||||
) {
|
||||
publisher = Some(p);
|
||||
if let Some(channel) = crate::monitor::take_frame_channel(target_id) {
|
||||
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
|
||||
// happy under `-D warnings`; attach on success, drop the delivery on Err.
|
||||
if let Ok(p) = FramePublisher::from_channel(
|
||||
channel,
|
||||
render_luid_low,
|
||||
render_luid_high,
|
||||
&device.device,
|
||||
&device.device_context,
|
||||
) {
|
||||
publisher = Some(p);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frames_since_try += 1;
|
||||
@@ -337,10 +350,10 @@ impl SwapChainProcessor {
|
||||
if !raw.is_null() {
|
||||
// SAFETY: `raw` is IddCx's live surface pointer (valid until the next
|
||||
// ReleaseAndAcquire); `from_raw_borrowed` does not consume the refcount.
|
||||
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) } {
|
||||
if let Ok(tex) = res.cast::<ID3D11Texture2D>() {
|
||||
p.publish(&tex);
|
||||
}
|
||||
if let Some(res) = unsafe { IDXGIResource::from_raw_borrowed(&raw) }
|
||||
&& let Ok(tex) = res.cast::<ID3D11Texture2D>()
|
||||
{
|
||||
p.publish(&tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ wdk-build.workspace = true
|
||||
[dependencies]
|
||||
wdk.workspace = true
|
||||
wdk-sys.workspace = true
|
||||
pf-driver-proto.workspace = true
|
||||
pf-umdf-util.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -14,8 +14,11 @@ instance (= player slot 0–3) with `CreateFile`, and polls it with buffered IOC
|
||||
**System** setup class;
|
||||
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
|
||||
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
|
||||
controller state the host publishes into a shared section `Global\pfxusb-shm-0`; a game's rumble
|
||||
(`SET_STATE`) is published back for the host to forward to the client.
|
||||
controller state the host publishes into an **unnamed** shared DATA section reached over the
|
||||
**sealed pad channel** (`design/gamepad-channel-sealing.md`): the host duplicates the section
|
||||
handle into this driver's WUDFHost, bootstrapped via the named `Global\pfxusb-boot-<index>`
|
||||
mailbox (`pf_driver_proto::gamepad::PadBootstrap`); a game's rumble (`SET_STATE`) is published
|
||||
back for the host to forward to the client.
|
||||
|
||||
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
|
||||
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
|
||||
@@ -37,11 +40,13 @@ GameInput admission additionally needs a `xinputhid` `UpperFilters` registry tri
|
||||
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
|
||||
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
|
||||
|
||||
## Shared-memory layout `Global\pfxusb-shm-0` (64 B) — host writes state, driver writes rumble
|
||||
## Shared-memory layout (unnamed DATA section, 64 B) — host writes state, driver writes rumble
|
||||
|
||||
`pf_driver_proto::gamepad::XusbShm` (the crate owns the offsets; both sides compile against it):
|
||||
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
|
||||
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
||||
`large @28` · `small @29`.
|
||||
`large @28` · `small @29` · health marks `@32/@36` · `pad_index u32 @40` (validated against the
|
||||
devnode's Location index when the delivered handle is mapped).
|
||||
|
||||
## Validated live (2026-06-22, maintainer's RTX test box)
|
||||
|
||||
@@ -66,7 +71,8 @@ the whole build/sign/stage flow in CI. The manual steps:
|
||||
## Host integration (done)
|
||||
|
||||
`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by
|
||||
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, writes
|
||||
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, delivers the unnamed DATA
|
||||
section over the sealed channel (`PadChannel`), writes
|
||||
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
||||
is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI
|
||||
(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via
|
||||
@@ -75,8 +81,8 @@ is **no ViGEmBus dependency** anymore. The driver is built + signed from source
|
||||
## Multi-pad
|
||||
|
||||
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
|
||||
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and maps its own
|
||||
`pfxusb-shm-<index>`. `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
||||
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and polls its own
|
||||
`pfxusb-boot-<index>` bootstrap mailbox (the delivered DATA section's `pad_index` is validated against it). `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
||||
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
|
||||
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
|
||||
index — which only routes shared memory.)
|
||||
|
||||
@@ -3,42 +3,39 @@
|
||||
//
|
||||
// xinput1_4.dll enumerates GUID_DEVINTERFACE_XUSB, opens the Nth instance (= player slot), and polls
|
||||
// it with buffered IOCTLs. We register the interface and answer those IOCTLs from controller state the
|
||||
// host publishes into a shared-memory section (`Global\pfxusb-shm-0`); a game's rumble (SET_STATE) is
|
||||
// published back for the host to forward. Byte formats are the source-verified xusb22 wire layout
|
||||
// (HIDMaestro driver/companion.c + nefarius/XInputHooker XUSB.h + ViGEm XUSB_REPORT).
|
||||
// host publishes into a shared DATA section; a game's rumble (SET_STATE) is published back for the
|
||||
// host to forward. Byte formats are the source-verified xusb22 wire layout (HIDMaestro
|
||||
// driver/companion.c + nefarius/XInputHooker XUSB.h + ViGEm XUSB_REPORT).
|
||||
//
|
||||
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
|
||||
// DATA section (`pf_driver_proto::gamepad::XusbShm`) is UNNAMED — we reach it only through a handle
|
||||
// the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named `Global\pfxusb-boot-<i>`
|
||||
// mailbox. The whole handshake + all shared-memory access lives in `pf_umdf_util` (audited unsafe
|
||||
// layer): this crate's channel/IOCTL/state logic is 100% SAFE Rust. The only `unsafe` here is the
|
||||
// unavoidable WDF setup FFI in DriverEntry/EvtDeviceAdd, each with a `// SAFETY:` proof.
|
||||
//
|
||||
// We answer the WAIT_* IOCTLs with STATUS_INVALID_DEVICE_REQUEST, which makes xinput1_4 fall back to
|
||||
// synchronous GET_STATE polling — so no manual queue / timer is needed for classic XInput.
|
||||
|
||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
use pf_driver_proto::gamepad::XusbShm;
|
||||
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
|
||||
use pf_umdf_util::nt_success;
|
||||
use pf_umdf_util::section::MappedView;
|
||||
use pf_umdf_util::wdf::{self, Request};
|
||||
use wdk_sys::{
|
||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA, GUID, NTSTATUS, PCUNICODE_STRING,
|
||||
PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
||||
WDF_DRIVER_CONFIG, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES,
|
||||
GUID, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
||||
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFDRIVER, WDFQUEUE,
|
||||
WDFREQUEST, call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
||||
};
|
||||
|
||||
// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
||||
// wdk_sys root; the value is stable WDM).
|
||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||
|
||||
/// The pad index this device serves (which `pfxusb-shm-<index>` section to map). The host stamps it
|
||||
/// into the device Location (`pszDeviceLocation`); the driver reads it in EvtDeviceAdd. With
|
||||
/// `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) each pad gets its own WUDFHost, so this
|
||||
/// static is per-pad — the basis for multi-pad.
|
||||
static SHM_INDEX: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
// ---- NTSTATUS ----
|
||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||
const STATUS_INVALID_DEVICE_REQUEST: NTSTATUS = 0xC000_0010u32 as NTSTATUS;
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
|
||||
// GUID_DEVINTERFACE_XUSB {EC87F1E3-C13B-4100-B5F7-8B84D54260CB} — what xinput1_4 enumerates + opens.
|
||||
const GUID_DEVINTERFACE_XUSB: GUID = GUID {
|
||||
@@ -70,27 +67,46 @@ const XUSB_VERSION: u16 = 0x0103;
|
||||
const WdfIoQueueDispatchParallel: i32 = 2;
|
||||
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
|
||||
|
||||
// ---- shared-memory layout (host ↔ driver), must match pf_driver_proto::gamepad::XusbShm ----
|
||||
// magic u32 @0 ("PFXU"); packet u32 @4 (host bumps on state change → dwPacketNumber); the XUSB_REPORT
|
||||
// payload @8: wButtons u16 @8, bLeftTrigger @10, bRightTrigger @11, sThumbLX i16 @12, LY @14, RX @16,
|
||||
// RY @18; rumble_seq u32 @24 (driver bumps on SET_STATE); rumble large @28, small @29;
|
||||
// driver_proto u32 @32 (we stamp GAMEPAD_PROTO_VERSION = attach signal for the host's health check);
|
||||
// driver_heartbeat u32 @36 (we bump per serviced IOCTL = the game-visible polling path moves).
|
||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004;
|
||||
const SHM_MAGIC: u32 = 0x5558_4650; // "PFXU" little-endian
|
||||
const SHM_SIZE: usize = 64;
|
||||
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
|
||||
// ---- the sealed host channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
|
||||
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::XUSB_MAGIC; // "PFXU"
|
||||
const SHM_SIZE: usize = core::mem::size_of::<XusbShm>();
|
||||
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
|
||||
|
||||
unsafe extern "system" {
|
||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||
fn CloseHandle(h: *mut c_void) -> i32;
|
||||
// XusbShm field offsets (host writes state, we answer XInput; we write rumble + health marks).
|
||||
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_LARGE: usize = core::mem::offset_of!(XusbShm, rumble_large);
|
||||
const OFF_RUMBLE_SMALL: usize = core::mem::offset_of!(XusbShm, rumble_small);
|
||||
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
|
||||
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(XusbShm, driver_heartbeat);
|
||||
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
|
||||
|
||||
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
|
||||
/// this static is per-pad). All shared-memory access + the bootstrap handshake live in `pf_umdf_util`.
|
||||
static CHANNEL: ChannelClient = ChannelClient::new();
|
||||
|
||||
/// This pad's channel config (magic/size/pad_index offset + our logger).
|
||||
fn channel_cfg() -> ChannelConfig {
|
||||
ChannelConfig {
|
||||
tag: "pf-xusb",
|
||||
boot_name_prefix: "Global\\pfxusb-boot-",
|
||||
data_magic: SHM_MAGIC,
|
||||
data_size: SHM_SIZE,
|
||||
pad_index_off: OFF_PAD_INDEX,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
fn log(s: &str) {
|
||||
if let Ok(c) = std::ffi::CString::new(s) {
|
||||
// SAFETY: c is a valid null-terminated string for the duration of the call.
|
||||
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||
}
|
||||
use std::io::Write;
|
||||
@@ -110,11 +126,11 @@ pub unsafe extern "system" fn driver_entry(
|
||||
registry_path: PCUNICODE_STRING,
|
||||
) -> NTSTATUS {
|
||||
log("[pf-xusb] DriverEntry");
|
||||
// SAFETY: zeroed config then Size + callback set.
|
||||
// SAFETY: a zeroed WDF_DRIVER_CONFIG is a valid all-null config; we then set Size + the callback.
|
||||
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
|
||||
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
|
||||
config.EvtDriverDeviceAdd = Some(evt_device_add);
|
||||
// SAFETY: all pointers valid; provided by the loader.
|
||||
// SAFETY: `driver`/`registry_path` are the loader-provided pointers; the config is valid.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDriverCreate,
|
||||
@@ -127,56 +143,11 @@ pub unsafe extern "system" fn driver_entry(
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), a NUL-terminated
|
||||
/// UTF-16 decimal string. Defaults to 0 (single-pad) if absent.
|
||||
fn query_shm_index(device: WDFDEVICE) -> u32 {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: device valid; property = LocationInformation; pool ignored in UMDF; mem receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceAllocAndQueryProperty,
|
||||
device,
|
||||
DEVICE_PROPERTY_LOCATION_INFORMATION,
|
||||
0,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut mem
|
||||
)
|
||||
};
|
||||
if !nt_success(st) || mem.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: mem valid.
|
||||
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
|
||||
as *const u16;
|
||||
if buf.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut idx: u32 = 0;
|
||||
let mut any = false;
|
||||
for i in 0..(len / 2).min(8) {
|
||||
// SAFETY: buf valid for len bytes; i < len/2.
|
||||
let c = unsafe { *buf.add(i) };
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
if (0x30..=0x39).contains(&c) {
|
||||
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any {
|
||||
idx
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
|
||||
log("[pf-xusb] EvtDeviceAdd");
|
||||
|
||||
let mut device: WDFDEVICE = core::ptr::null_mut();
|
||||
// SAFETY: device_init valid; attributes null; device receives the handle.
|
||||
// SAFETY: `device_init` is the framework-provided init; attributes null; `device` receives it.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceCreate,
|
||||
@@ -190,12 +161,14 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
return st;
|
||||
}
|
||||
|
||||
let idx = query_shm_index(device);
|
||||
SHM_INDEX.store(idx, Ordering::Relaxed);
|
||||
// SAFETY: `device` is the live device just created — the exact contract `query_location_index`
|
||||
// requires.
|
||||
let idx = unsafe { wdf::query_location_index(device) };
|
||||
CHANNEL.set_index(idx);
|
||||
dbglog!("[pf-xusb] shm index = {idx}");
|
||||
|
||||
// Register the XUSB device interface (no reference string) — what xinput1_4 enumerates + opens.
|
||||
// SAFETY: device valid; GUID static; null reference string.
|
||||
// SAFETY: `device` is live; the GUID is a static; null reference string.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceCreateDeviceInterface,
|
||||
@@ -213,7 +186,7 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
}
|
||||
|
||||
// Default parallel queue: all the XUSB IOCTLs land here.
|
||||
// SAFETY: zeroed config then fields set; Size matches the struct.
|
||||
// SAFETY: a zeroed WDF_IO_QUEUE_CONFIG is valid; we then set Size + the fields we use.
|
||||
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
|
||||
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
||||
qcfg.DispatchType = WdfIoQueueDispatchParallel;
|
||||
@@ -222,7 +195,7 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
|
||||
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
|
||||
let mut queue: WDFQUEUE = core::ptr::null_mut();
|
||||
// SAFETY: device + config valid; attributes null; queue receives the handle.
|
||||
// SAFETY: `device` + `qcfg` are valid; attributes null; `queue` receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfIoQueueCreate,
|
||||
@@ -237,93 +210,69 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
|
||||
return st;
|
||||
}
|
||||
|
||||
// Tell the host we're alive on the section (its driver-attach health check keys off this).
|
||||
touch_driver_marks();
|
||||
// Run the sealed-channel handshake on a worker (must NOT block EvtDeviceAdd): publish our pid in
|
||||
// the bootstrap mailbox and poll for the host's delivered DATA handle, so the pad attaches (and
|
||||
// the host's driver-attach health check goes green) even before any game polls XInput. Bounded;
|
||||
// a later host (or a re-delivery) is still picked up by the per-IOCTL pump. This closure is 100%
|
||||
// safe — the whole channel state machine lives in pf_umdf_util.
|
||||
std::thread::spawn(|| {
|
||||
let cfg = channel_cfg();
|
||||
for _ in 0..500 {
|
||||
if let Some(v) = CHANNEL.pump(&cfg) {
|
||||
touch_driver_marks(v);
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
log(
|
||||
"[pf-xusb] no sealed-channel delivery within 10s (host absent, or host/driver version mismatch — see above)",
|
||||
);
|
||||
});
|
||||
|
||||
log("[pf-xusb] device ready (XUSB interface registered)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// Open + map the host's shared section and run `f` against the mapped base if magic is valid, then
|
||||
// unmap. Re-mapped per access (the host may recreate the section across restarts).
|
||||
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
|
||||
let name: Vec<u16> = format!("Global\\pfxusb-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: h is a valid mapping handle; map the whole section; the view keeps it alive.
|
||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
||||
unsafe { CloseHandle(h) };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: view points at >= 4 mapped bytes.
|
||||
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
|
||||
if magic == SHM_MAGIC {
|
||||
f(view);
|
||||
}
|
||||
// SAFETY: view came from MapViewOfFile.
|
||||
unsafe { UnmapViewOfFile(view as *const c_void) };
|
||||
}
|
||||
|
||||
/// The current controller state from shared memory (zeros / neutral if the host hasn't connected).
|
||||
/// The current controller state from the attached DATA section (zeros / neutral when unattached).
|
||||
/// Returns `(dwPacketNumber, wButtons, lt, rt, lx, ly, rx, ry)`.
|
||||
fn read_state() -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
||||
let mut out = (0u32, 0u16, 0u8, 0u8, 0i16, 0i16, 0i16, 0i16);
|
||||
with_shm(|v| {
|
||||
// SAFETY: v points at a mapped SHM_SIZE section with valid magic.
|
||||
unsafe {
|
||||
out.0 = core::ptr::read_unaligned(v.add(4) as *const u32);
|
||||
out.1 = core::ptr::read_unaligned(v.add(8) as *const u16);
|
||||
out.2 = *v.add(10);
|
||||
out.3 = *v.add(11);
|
||||
out.4 = core::ptr::read_unaligned(v.add(12) as *const i16);
|
||||
out.5 = core::ptr::read_unaligned(v.add(14) as *const i16);
|
||||
out.6 = core::ptr::read_unaligned(v.add(16) as *const i16);
|
||||
out.7 = core::ptr::read_unaligned(v.add(18) as *const i16);
|
||||
}
|
||||
});
|
||||
out
|
||||
fn read_state(data: Option<&MappedView>) -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
||||
match data {
|
||||
Some(v) => (
|
||||
v.read_u32(OFF_PACKET),
|
||||
v.read_u16(OFF_BUTTONS),
|
||||
v.read_u8(OFF_LT),
|
||||
v.read_u8(OFF_RT),
|
||||
v.read_i16(OFF_LX),
|
||||
v.read_i16(OFF_LY),
|
||||
v.read_i16(OFF_RX),
|
||||
v.read_i16(OFF_RY),
|
||||
),
|
||||
None => (0, 0, 0, 0, 0, 0, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stamp the driver health marks the host watches: `driver_proto` @32 (the attach signal,
|
||||
/// idempotent) and `driver_heartbeat` @36 (+1). Called at device add and on every serviced IOCTL,
|
||||
/// so the host can tell "driver bound and alive" apart from "driver package missing/failed to
|
||||
/// bind" and see the game-visible polling path advance. No-op until the host's section exists
|
||||
/// (with_shm re-opens per access, so a section created after we started still gets marked).
|
||||
fn touch_driver_marks() {
|
||||
with_shm(|v| {
|
||||
// SAFETY: v points at a mapped SHM_SIZE section with valid magic; proto @32, heartbeat @36.
|
||||
unsafe {
|
||||
core::ptr::write_unaligned(v.add(32) as *mut u32, GAMEPAD_PROTO_VERSION);
|
||||
let hb = v.add(36) as *mut u32;
|
||||
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
|
||||
}
|
||||
});
|
||||
/// Stamp the driver health marks the host watches: `driver_proto` (the attach signal, idempotent)
|
||||
/// and `driver_heartbeat` (+1). Called once the channel attaches and on every serviced IOCTL, so the
|
||||
/// host can tell "driver bound and alive" apart from "driver package missing/failed to bind" and see
|
||||
/// the game-visible polling path advance.
|
||||
fn touch_driver_marks(data: &MappedView) {
|
||||
data.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
|
||||
let hb = data.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
|
||||
data.write_u32(OFF_DRIVER_HEARTBEAT, hb);
|
||||
}
|
||||
|
||||
/// Publish a game's rumble (from SET_STATE) into shared memory for the host to forward.
|
||||
fn publish_rumble(large: u8, small: u8) {
|
||||
with_shm(|v| {
|
||||
// SAFETY: v points at a mapped SHM_SIZE section; rumble_seq @24, large @28, small @29.
|
||||
unsafe {
|
||||
*v.add(28) = large;
|
||||
*v.add(29) = small;
|
||||
let seqp = v.add(24) as *mut u32;
|
||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
||||
core::ptr::write_unaligned(seqp, seq);
|
||||
}
|
||||
});
|
||||
/// Publish a game's rumble (from SET_STATE) into the DATA section for the host to forward.
|
||||
fn publish_rumble(data: Option<&MappedView>, large: u8, small: u8) {
|
||||
let Some(v) = data else { return };
|
||||
v.write_u8(OFF_RUMBLE_LARGE, large);
|
||||
v.write_u8(OFF_RUMBLE_SMALL, small);
|
||||
let seq = v.read_u32(OFF_RUMBLE_SEQ).wrapping_add(1);
|
||||
v.write_u32(OFF_RUMBLE_SEQ, seq);
|
||||
}
|
||||
|
||||
// Build the 29-byte GET_STATE buffer (the layout xinput1_4 parses).
|
||||
fn build_get_state() -> [u8; 29] {
|
||||
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state();
|
||||
fn build_get_state(data: Option<&MappedView>) -> [u8; 29] {
|
||||
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state(data);
|
||||
let mut s = [0u8; 29];
|
||||
s[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||
s[2] = 0x01; // device count
|
||||
@@ -374,11 +323,20 @@ extern "C" fn evt_io_device_control(
|
||||
input_len: usize,
|
||||
ioctl: ULONG,
|
||||
) {
|
||||
// Health marks first: attach signal + heartbeat (also covers a section the host created after
|
||||
// this device started — the marks land on the next XInput poll).
|
||||
touch_driver_marks();
|
||||
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
|
||||
// contract `Request::new` requires. From here everything is safe (the token owns completion).
|
||||
let request = unsafe { Request::new(request) };
|
||||
|
||||
// Sealed-channel pump + health marks first: adopt a (late) delivery, detach when the host's
|
||||
// mailbox is gone, and stamp the attach/heartbeat marks the host watches (also covers a host
|
||||
// started after this device — the pump attaches on the next XInput poll).
|
||||
let data = CHANNEL.pump(&channel_cfg());
|
||||
if let Some(v) = data {
|
||||
touch_driver_marks(v);
|
||||
}
|
||||
|
||||
let status: NTSTATUS = match ioctl {
|
||||
IOCTL_XUSB_GET_INFORMATION => copy_to_output(request, &build_information()),
|
||||
IOCTL_XUSB_GET_INFORMATION => request.copy_to_output(&build_information()),
|
||||
IOCTL_XUSB_GET_INFORMATION_EX => {
|
||||
let mut ex = [0u8; 64];
|
||||
ex[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||
@@ -387,21 +345,19 @@ extern "C" fn evt_io_device_control(
|
||||
ex[8..10].copy_from_slice(&XUSB_VID.to_le_bytes());
|
||||
ex[10..12].copy_from_slice(&XUSB_PID.to_le_bytes());
|
||||
let n = output_len.min(64);
|
||||
copy_to_output(request, &ex[..n])
|
||||
request.copy_to_output(&ex[..n])
|
||||
}
|
||||
IOCTL_XUSB_GET_CAPABILITIES => {
|
||||
if output_len >= 36 {
|
||||
copy_to_output(request, &build_caps_v2())
|
||||
request.copy_to_output(&build_caps_v2())
|
||||
} else {
|
||||
copy_to_output(request, &CAPS_V1)
|
||||
request.copy_to_output(&CAPS_V1)
|
||||
}
|
||||
}
|
||||
IOCTL_XUSB_GET_STATE => copy_to_output(request, &build_get_state()),
|
||||
IOCTL_XUSB_GET_LED_STATE => copy_to_output(request, &[0x00, 0x00, 0x06]),
|
||||
IOCTL_XUSB_GET_BATTERY_INFORMATION => {
|
||||
copy_to_output(request, &[0x00, 0x01, 0x03, 0x00])
|
||||
}
|
||||
IOCTL_XUSB_SET_STATE => on_set_state(request),
|
||||
IOCTL_XUSB_GET_STATE => request.copy_to_output(&build_get_state(data)),
|
||||
IOCTL_XUSB_GET_LED_STATE => request.copy_to_output(&[0x00, 0x00, 0x06]),
|
||||
IOCTL_XUSB_GET_BATTERY_INFORMATION => request.copy_to_output(&[0x00, 0x01, 0x03, 0x00]),
|
||||
IOCTL_XUSB_SET_STATE => on_set_state(&request, data),
|
||||
IOCTL_XUSB_POWER_DOWN | IOCTL_XUSB_GET_XINPUT_MANAGEMENT_DRIVER => STATUS_SUCCESS,
|
||||
// Decline the async waits → xinput1_4 falls back to synchronous GET_STATE polling.
|
||||
IOCTL_XUSB_WAIT_GUIDE_BUTTON | IOCTL_XUSB_WAIT_FOR_INPUT => STATUS_INVALID_DEVICE_REQUEST,
|
||||
@@ -410,78 +366,29 @@ extern "C" fn evt_io_device_control(
|
||||
STATUS_INVALID_DEVICE_REQUEST
|
||||
}
|
||||
};
|
||||
// SAFETY: request valid and not forwarded.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
||||
request.complete(status);
|
||||
}
|
||||
|
||||
// SET_STATE: the rumble packet. Classic xusb22 layout is small; the motor bytes sit near the end.
|
||||
// We publish a best-effort (large = byte 3, small = byte 4 for the 5-byte form) and log the raw bytes
|
||||
// We publish a best-effort (large = byte 2, small = byte 3 for the 5-byte form) and log the raw bytes
|
||||
// so the exact offsets can be confirmed against a real pad.
|
||||
fn on_set_state(request: WDFREQUEST) -> NTSTATUS {
|
||||
let mut inmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
|
||||
};
|
||||
if nt_success(st) {
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: inmem valid.
|
||||
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
||||
as *const u8;
|
||||
if !p.is_null() && len >= 2 {
|
||||
let n = len.min(8);
|
||||
// SAFETY: p valid for len bytes; read at most n.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(p, n) };
|
||||
let mut hex = String::new();
|
||||
for b in bytes {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
dbglog!("[pf-xusb] SET_STATE len={len} data: {hex}");
|
||||
// Observed 5-byte form {00, led, largeMotor, smallMotor, subcmd}: subcmd 0x02 = rumble
|
||||
// (large/low-freq at [2], small/high-freq at [3]); 0x01 = player-LED set (ignored).
|
||||
// 4-byte = raw XINPUT_VIBRATION → the two motor hi bytes.
|
||||
if len >= 5 && bytes[4] == 0x02 {
|
||||
publish_rumble(bytes[2], bytes[3]);
|
||||
} else if len == 4 {
|
||||
publish_rumble(bytes[1], bytes[3]);
|
||||
}
|
||||
fn on_set_state(request: &Request, data: Option<&MappedView>) -> NTSTATUS {
|
||||
if let Ok((bytes, len)) = request.input_bytes(8)
|
||||
&& len >= 2
|
||||
{
|
||||
let mut hex = String::new();
|
||||
for b in &bytes {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
dbglog!("[pf-xusb] SET_STATE len={len} data: {hex}");
|
||||
// Observed 5-byte form {00, led, largeMotor, smallMotor, subcmd}: subcmd 0x02 = rumble
|
||||
// (large/low-freq at [2], small/high-freq at [3]); 0x01 = player-LED set (ignored).
|
||||
// 4-byte = raw XINPUT_VIBRATION → the two motor hi bytes.
|
||||
if len >= 5 && bytes[4] == 0x02 {
|
||||
publish_rumble(data, bytes[2], bytes[3]);
|
||||
} else if len == 4 {
|
||||
publish_rumble(data, bytes[1], bytes[3]);
|
||||
}
|
||||
}
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; mem receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: mem valid; outlen receives the buffer size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: request valid.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
||||
};
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//! code — handled at the call site in STEP 5).
|
||||
#![no_std]
|
||||
#![allow(non_snake_case, clippy::missing_safety_doc)]
|
||||
// P0 lint (audit §8): require explicit `unsafe {}` blocks inside `unsafe fn`s.
|
||||
// P0 lint (audit §8): require explicit `unsafe {}` blocks inside `unsafe fn`s + a `// SAFETY:` proof on
|
||||
// each (this crate is the IddCx DDI dispatch layer — inherently unsafe, so audited, not unsafe-free).
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
pub use wdk_sys::iddcx;
|
||||
|
||||
@@ -36,6 +38,7 @@ unsafe fn ddi<T: Copy>(index: i32) -> T {
|
||||
let table = (&raw const iddcx::IddFunctions).cast::<iddcx::PFN_IDD_CX>();
|
||||
// SAFETY: `index` is a valid IddCx table slot; the slot holds a `PFN_*` whose layout is `T`.
|
||||
let slot = unsafe { table.add(index as usize) };
|
||||
// SAFETY: `slot` points at the `index`th (in-bounds) populated table entry, a `PFN_*` of layout `T`.
|
||||
unsafe { slot.cast::<T>().read() }
|
||||
}
|
||||
|
||||
@@ -62,7 +65,10 @@ macro_rules! iddcx_ddi {
|
||||
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
||||
#[inline]
|
||||
pub unsafe fn $name( $( $arg: $aty ),* ) -> NTSTATUS {
|
||||
// SAFETY: `$idx`/`$pfn` are the matched IddCx table index + PFN type (pinned by this macro
|
||||
// invocation), and the table is populated once the driver is loaded (this fn's contract).
|
||||
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
||||
// SAFETY: only reads the stub-provided globals pointer; valid post-load per the contract.
|
||||
let g = unsafe { globals() };
|
||||
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
||||
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
||||
@@ -79,7 +85,10 @@ macro_rules! iddcx_ddi {
|
||||
/// Call only after the driver is loaded by IddCx; pointers must satisfy the IddCx contract.
|
||||
#[inline]
|
||||
pub unsafe fn $name( $( $arg: $aty ),* ) {
|
||||
// SAFETY: `$idx`/`$pfn` are the matched IddCx table index + PFN type (pinned by this macro
|
||||
// invocation), and the table is populated once the driver is loaded (this fn's contract).
|
||||
let f: iddcx::$pfn = unsafe { ddi(iddcx::_IDDFUNCENUM::$idx) };
|
||||
// SAFETY: only reads the stub-provided globals pointer; valid post-load per the contract.
|
||||
let g = unsafe { globals() };
|
||||
// SAFETY: dispatching a populated DDI with the stub globals and caller-valid args.
|
||||
unsafe { (f.unwrap())(g, $( $arg ),* ) }
|
||||
|
||||
Reference in New Issue
Block a user