refactor(inject/windows/dualsense): convert to UhidManager<DsWinProto> (3.3)
DualSenseWindowsManager becomes a pub type alias of UhidManager<DsWinProto>; the proto supplies the UMDF sealed-channel open (+ success log), the DsState mappers (identical to linux/dualsense.rs, paddle fold included), and the section feedback poll. Lifecycle, dedup, and heartbeat come from the shared skeleton — behavior-identical, same log lines (LABEL DualSense/Windows + the driver-install hint). DsWinPad goes pub (it appears as type Pad in the impl of the public PadProto trait — E0446 otherwise; the Linux pads were already pub). Verified on the Windows CI VM .133 (same pinned 1.96.0 MSVC toolchain + Public-path FFmpeg/LLVM the runner uses): cargo clippy -p punktfunk-host --all-targets -- -D warnings EXITCODE 0 at the DS4-conversion tip (.173 was down; .133 carries the identical toolchain). Part of G12/3.3 (§3a.4 commit 5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,17 +18,16 @@
|
|||||||
//! must already be installed; the installer stages it.)
|
//! must already be installed; the installer stages it.)
|
||||||
|
|
||||||
use super::dualsense_proto::{
|
use super::dualsense_proto::{
|
||||||
parse_ds_output, serialize_state, DsFeedback, DsState, HidoutDedup, DS_INPUT_REPORT_LEN,
|
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||||
DS_TOUCH_H, DS_TOUCH_W,
|
DS_TOUCH_W,
|
||||||
};
|
};
|
||||||
use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx};
|
use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx};
|
||||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||||
use crate::inject::pad_gate::PadGate;
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
use punktfunk_core::quic::RichInput;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::sync::atomic::{fence, AtomicU32, Ordering};
|
use std::sync::atomic::{fence, AtomicU32, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
use windows::core::{w, GUID, PCWSTR};
|
use windows::core::{w, GUID, PCWSTR};
|
||||||
use windows::Win32::Devices::Enumeration::Pnp::{
|
use windows::Win32::Devices::Enumeration::Pnp::{
|
||||||
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
|
||||||
@@ -60,7 +59,9 @@ pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUAL
|
|||||||
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
||||||
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
||||||
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
|
/// Dropping it removes the devnode (`SwDeviceClose`) and closes both sections.
|
||||||
struct DsWinPad {
|
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait), like the
|
||||||
|
/// Linux pads.
|
||||||
|
pub struct DsWinPad {
|
||||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||||
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
/// `None` falls back to an out-of-band `pf_dualsense` devnode (installer/devgen).
|
||||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||||
@@ -351,75 +352,50 @@ impl DsWinPad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All virtual DualSense pads of a session — the Windows analogue of
|
/// The Windows-DualSense half of the shared stateful manager (see [`PadProto`]): the UMDF
|
||||||
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface so the session input
|
/// sealed-channel open, the same [`DsState`] mappers as `linux/dualsense.rs`, and the section
|
||||||
/// thread drives either backend identically.
|
/// feedback poll. Lifecycle (slot table, unplug sweep, heartbeat, dedup) lives in [`UhidManager`].
|
||||||
pub struct DualSenseWindowsManager {
|
pub struct DsWinProto {
|
||||||
pads: Vec<Option<DsWinPad>>,
|
|
||||||
state: Vec<DsState>,
|
|
||||||
last_rumble: Vec<(u16, u16)>,
|
|
||||||
/// Last rich feedback (lightbar / player LEDs / adaptive triggers) forwarded per pad, so an
|
|
||||||
/// output report that only changed the rumble doesn't re-send unchanged 0xCD feedback.
|
|
||||||
hidout_dedup: Vec<HidoutDedup>,
|
|
||||||
last_write: Vec<Instant>,
|
|
||||||
/// Create-retry gate: a transient UMDF-channel failure backs off and retries instead of
|
|
||||||
/// permanently disabling every pad for the session.
|
|
||||||
gate: PadGate,
|
|
||||||
/// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button
|
/// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button
|
||||||
/// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualsense.rs`.
|
/// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualsense.rs`.
|
||||||
remap: crate::inject::steam_remap::RemapConfig,
|
remap: crate::inject::steam_remap::RemapConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DualSenseWindowsManager {
|
impl Default for DsWinProto {
|
||||||
fn default() -> DualSenseWindowsManager {
|
fn default() -> DsWinProto {
|
||||||
DualSenseWindowsManager::new()
|
DsWinProto {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DualSenseWindowsManager {
|
|
||||||
pub fn new() -> DualSenseWindowsManager {
|
|
||||||
DualSenseWindowsManager {
|
|
||||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
|
||||||
state: vec![DsState::neutral(); MAX_PADS],
|
|
||||||
last_rumble: vec![(0, 0); MAX_PADS],
|
|
||||||
hidout_dedup: vec![HidoutDedup::default(); MAX_PADS],
|
|
||||||
last_write: vec![Instant::now(); MAX_PADS],
|
|
||||||
gate: PadGate::new(),
|
|
||||||
remap: crate::inject::steam_remap::RemapConfig::from_env(),
|
remap: crate::inject::steam_remap::RemapConfig::from_env(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle one decoded controller event (create/destroy by mask, then merge button/stick state).
|
impl PadProto for DsWinProto {
|
||||||
pub fn handle(&mut self, ev: &GamepadEvent) {
|
type Pad = DsWinPad;
|
||||||
match ev {
|
type State = DsState;
|
||||||
GamepadEvent::Arrival { index, kind, .. } => {
|
const LABEL: &'static str = "DualSense/Windows";
|
||||||
tracing::info!(index, kind, "controller arrival (DualSense/Windows)");
|
const DEVICE: &'static str = "DualSense";
|
||||||
self.ensure(*index as usize);
|
const CREATE_HINT: &'static str =
|
||||||
|
" (install/repair: punktfunk-host.exe driver install --gamepad)";
|
||||||
|
|
||||||
|
fn open(&mut self, idx: u8) -> Result<DsWinPad> {
|
||||||
|
let p = DsWinPad::open(idx)?;
|
||||||
|
tracing::info!(
|
||||||
|
index = idx,
|
||||||
|
"virtual DualSense created (Windows UMDF shm channel)"
|
||||||
|
);
|
||||||
|
Ok(p)
|
||||||
}
|
}
|
||||||
GamepadEvent::State(f) => {
|
|
||||||
let idx = f.index as usize;
|
fn neutral(&self) -> DsState {
|
||||||
if idx >= MAX_PADS {
|
DsState::neutral()
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
for (i, slot) in self.pads.iter_mut().enumerate() {
|
|
||||||
if slot.is_some() && f.active_mask & (1 << i) == 0 {
|
/// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (rich-
|
||||||
tracing::info!(index = i, "controller unplugged (DualSense/Windows)");
|
/// plane fields that must survive a button-only frame) — exactly as `linux/dualsense.rs` does.
|
||||||
*slot = None;
|
fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState {
|
||||||
self.state[i] = DsState::neutral();
|
|
||||||
self.last_rumble[i] = (0, 0);
|
|
||||||
self.hidout_dedup[i].clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f.active_mask & (1 << idx) == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.ensure(idx);
|
|
||||||
let prev = self.state[idx];
|
|
||||||
// Steam back grips have no DualSense slot — fold them onto standard buttons per the
|
// Steam back grips have no DualSense slot — fold them onto standard buttons per the
|
||||||
// configured policy (default drop) so they aren't silently lost, exactly as
|
// configured policy (default drop) so they aren't silently lost.
|
||||||
// `linux/dualsense.rs` does.
|
let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles);
|
||||||
let buttons =
|
|
||||||
crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles);
|
|
||||||
let mut s = DsState::from_gamepad(
|
let mut s = DsState::from_gamepad(
|
||||||
buttons,
|
buttons,
|
||||||
f.ls_x,
|
f.ls_x,
|
||||||
@@ -433,98 +409,33 @@ impl DualSenseWindowsManager {
|
|||||||
s.gyro = prev.gyro;
|
s.gyro = prev.gyro;
|
||||||
s.accel = prev.accel;
|
s.accel = prev.accel;
|
||||||
s.touch_click = prev.touch_click;
|
s.touch_click = prev.touch_click;
|
||||||
self.state[idx] = s;
|
s
|
||||||
self.write(idx);
|
}
|
||||||
|
|
||||||
|
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
|
||||||
|
/// split the one touchpad left/right, pad clicks ride touch_click.
|
||||||
|
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
|
||||||
|
st.apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_state(&self, pad: &mut DsWinPad, st: &DsState) {
|
||||||
|
pad.write_state(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll the section for a game's feedback: motor rumble on the universal 0xCA plane, the rich
|
||||||
|
/// lightbar/player-LED/trigger events on the 0xCD plane.
|
||||||
|
fn service(&self, pad: &mut DsWinPad, idx: u8) -> PadFeedback {
|
||||||
|
let fb = pad.service(idx);
|
||||||
|
PadFeedback {
|
||||||
|
rumble: fb.rumble,
|
||||||
|
hidout: fb.hidout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply one rich client→host event (touchpad contact / motion sample) to an existing pad.
|
/// All virtual DualSense pads of a session — the Windows analogue of
|
||||||
pub fn apply_rich(&mut self, rich: RichInput) {
|
/// [`DualSenseManager`](super::dualsense::DualSenseManager). Same method surface (via the shared
|
||||||
let idx = match rich {
|
/// [`UhidManager`]) so the session input thread drives either backend identically. The heartbeat
|
||||||
RichInput::Touchpad { pad, .. }
|
/// keeps the section fresh (the driver's timer streams whatever's in it) — parity with the UHID
|
||||||
| RichInput::Motion { pad, .. }
|
/// backend's silence heartbeat.
|
||||||
| RichInput::TouchpadEx { pad, .. } => pad as usize,
|
pub type DualSenseWindowsManager = UhidManager<DsWinProto>;
|
||||||
};
|
|
||||||
if idx >= MAX_PADS || self.pads[idx].is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam
|
|
||||||
// dual pads split the one touchpad left/right, pad clicks ride touch_click.
|
|
||||||
self.state[idx].apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
|
|
||||||
self.write(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&mut self, idx: usize) {
|
|
||||||
let st = self.state[idx];
|
|
||||||
if let Some(pad) = self.pads[idx].as_mut() {
|
|
||||||
pad.write_state(&st);
|
|
||||||
}
|
|
||||||
self.last_write[idx] = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-emit each live pad's current report if it's been silent for `max_gap` (the driver's timer
|
|
||||||
/// streams whatever's in the section, so this just keeps the section fresh / future-proofs parity
|
|
||||||
/// with the UHID backend's heartbeat).
|
|
||||||
pub fn heartbeat(&mut self, max_gap: Duration) {
|
|
||||||
let now = Instant::now();
|
|
||||||
for i in 0..self.pads.len() {
|
|
||||||
if self.pads[i].is_some() && now.duration_since(self.last_write[i]) >= max_gap {
|
|
||||||
self.write(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure(&mut self, idx: usize) {
|
|
||||||
if idx >= MAX_PADS || self.pads[idx].is_some() || !self.gate.allow(Instant::now()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match DsWinPad::open(idx as u8) {
|
|
||||||
Ok(p) => {
|
|
||||||
tracing::info!(
|
|
||||||
index = idx,
|
|
||||||
"virtual DualSense created (Windows UMDF shm channel)"
|
|
||||||
);
|
|
||||||
self.pads[idx] = Some(p);
|
|
||||||
self.state[idx] = DsState::neutral();
|
|
||||||
self.last_rumble[idx] = (0, 0);
|
|
||||||
self.hidout_dedup[idx].clear();
|
|
||||||
self.last_write[idx] = Instant::now();
|
|
||||||
self.gate.on_success();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(error = %format!("{e:#}"), "virtual DualSense creation failed — retrying with backoff (install/repair: punktfunk-host.exe driver install --gamepad)");
|
|
||||||
self.gate.on_failure(Instant::now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service every pad: poll the section for a game's feedback. `rumble` fires `(index, low, high)`
|
|
||||||
/// only on change (universal 0xCA plane); `hidout` fires for each rich DualSense feedback event
|
|
||||||
/// (lightbar / player LEDs / adaptive triggers — 0xCD plane).
|
|
||||||
pub fn pump(
|
|
||||||
&mut self,
|
|
||||||
mut rumble: impl FnMut(u16, u16, u16),
|
|
||||||
mut hidout: impl FnMut(HidOutput),
|
|
||||||
) {
|
|
||||||
for i in 0..self.pads.len() {
|
|
||||||
let Some(pad) = self.pads[i].as_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let fb = pad.service(i as u8);
|
|
||||||
if let Some(r) = fb.rumble {
|
|
||||||
if self.last_rumble[i] != r {
|
|
||||||
self.last_rumble[i] = r;
|
|
||||||
rumble(i as u16, r.0, r.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for h in fb.hidout {
|
|
||||||
// Skip rich feedback that repeats the last-forwarded value (the game's output report
|
|
||||||
// re-sends unchanged lightbar/LED/trigger state alongside every rumble update).
|
|
||||||
if self.hidout_dedup[i].should_forward(&h) {
|
|
||||||
hidout(h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user