Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8e8529cd7 | |||
| 4201851c7f |
@@ -30,6 +30,16 @@ file with `scripts/gen-third-party-notices.sh` when the dependency tree changes.
|
||||
|
||||
## Before you push
|
||||
|
||||
Enable the repo git hooks once per clone — they run the exact rustfmt gates CI runs (main
|
||||
workspace + the UMDF driver workspace) on every commit and push, so a push can never fail CI
|
||||
on formatting alone:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/git-hooks
|
||||
```
|
||||
|
||||
Then the usual full pass:
|
||||
|
||||
```sh
|
||||
cargo fmt --all --check
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
@@ -22,9 +22,9 @@ pub(crate) enum GlyphStyle {
|
||||
impl GlyphStyle {
|
||||
pub(crate) fn from_pref(pref: Option<GamepadPref>) -> GlyphStyle {
|
||||
match pref {
|
||||
Some(
|
||||
GamepadPref::DualSense | GamepadPref::DualSenseEdge | GamepadPref::DualShock4,
|
||||
) => GlyphStyle::Shapes,
|
||||
Some(GamepadPref::DualSense | GamepadPref::DualSenseEdge | GamepadPref::DualShock4) => {
|
||||
GlyphStyle::Shapes
|
||||
}
|
||||
Some(_) => GlyphStyle::Letters,
|
||||
None => GlyphStyle::Keyboard,
|
||||
}
|
||||
|
||||
@@ -538,10 +538,11 @@ pub mod gamepad {
|
||||
/// `device_type` = DualSense Edge (`VID_054C&PID_0DF2` HID identity — the DualSense report
|
||||
/// codec plus the four native back/Fn button bits).
|
||||
pub const DEVTYPE_DUALSENSE_EDGE: u8 = 2;
|
||||
/// `device_type` = **N4-spike** Steam Deck identity (`VID_28DE&PID_1205`). Exists only for
|
||||
/// the `deck-windows-spike` go/no-go probe (does Steam Input on Windows promote a
|
||||
/// software-devnode HID Deck?) — never stamped by a session.
|
||||
pub const DEVTYPE_STEAMDECK_SPIKE: u8 = 3;
|
||||
/// `device_type` = Steam Deck controller (`VID_28DE&PID_1205` HID identity, the captured
|
||||
/// controller-interface descriptor + the Steam `0x83`/`0xAE` feature contract). Promoted by
|
||||
/// Steam Input on Windows when the devnode's synthesized USB hardware ids carry `&MI_02`
|
||||
/// (the wired controller interface — the N4-spike finding).
|
||||
pub const DEVTYPE_STEAMDECK: u8 = 3;
|
||||
|
||||
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
|
||||
/// the host's positive "driver is alive on this section" signal (health check + version audit).
|
||||
|
||||
@@ -427,47 +427,6 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
// events on desktop, and the door Steam's on-screen keyboard types through under
|
||||
// gamescope). Toggled edge-wise — start/stop are not free on Wayland.
|
||||
let mut text_input_on = false;
|
||||
// One-shot on-glass touch diagnostics. Under the Deck's game-mode gamescope, Steam Input
|
||||
// owns the physical touchscreen and by default emulates it as a virtual trackpad/mouse —
|
||||
// so the app may see MouseMotion/MouseButton instead of the Finger* events the touch-mode
|
||||
// engine feeds on (which kills BOTH trackpad and passthrough at once). Set
|
||||
// `PUNKTFUNK_TOUCH_DEBUG=1` to log every raw finger AND mouse event: one run tells us
|
||||
// whether native wl_touch is being delivered (Finger* with direct=true) or intercepted.
|
||||
let touch_debug = std::env::var_os("PUNKTFUNK_TOUCH_DEBUG").is_some();
|
||||
// Under the Deck's game-mode gamescope the session binary's stderr is swallowed by Steam's
|
||||
// reaper, so ALSO mirror the debug lines to a file in the app data dir (host-visible at
|
||||
// ~/.var/app/io.unom.Punktfunk/…), pulled over SSH after a run.
|
||||
let mut touch_log: Option<std::fs::File> = touch_debug
|
||||
.then(|| {
|
||||
let dir = std::env::var_os("XDG_DATA_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.or_else(|| {
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/share"))
|
||||
})
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let path = dir.join("punktfunk-touch-debug.log");
|
||||
match std::fs::OpenOptions::new().create(true).append(true).open(&path) {
|
||||
Ok(f) => {
|
||||
tracing::info!(path = %path.display(), "touch-debug: mirroring to file");
|
||||
Some(f)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "touch-debug: file sink open failed");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
// Defined after `touch_log` so the literal identifier resolves to that local; a no-op when
|
||||
// the sink is absent (env unset or open failed).
|
||||
macro_rules! touch_file_log {
|
||||
($($arg:tt)*) => {
|
||||
if let Some(f) = touch_log.as_mut() {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(f, $($arg)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let outcome = 'main: loop {
|
||||
// --- SDL events (input, window, gamepads) ---------------------------------------
|
||||
@@ -599,19 +558,11 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
}
|
||||
}
|
||||
Event::MouseMotion { xrel, yrel, .. } => {
|
||||
if touch_debug {
|
||||
tracing::info!(xrel, yrel, "touch-debug: MouseMotion");
|
||||
touch_file_log!("MouseMotion xrel={xrel} yrel={yrel}");
|
||||
}
|
||||
if let Some(cap) = stream.as_mut().and_then(|s| s.capture.as_mut()) {
|
||||
cap.on_motion(xrel, yrel);
|
||||
}
|
||||
}
|
||||
Event::MouseButtonDown { mouse_btn, .. } => {
|
||||
if touch_debug {
|
||||
tracing::info!(?mouse_btn, "touch-debug: MouseButtonDown");
|
||||
touch_file_log!("MouseButtonDown mouse_btn={mouse_btn:?}");
|
||||
}
|
||||
if let Some(cap) = stream.as_mut().and_then(|s| s.capture.as_mut()) {
|
||||
if !cap.captured() {
|
||||
// The engaging click is suppressed toward the host.
|
||||
@@ -623,10 +574,6 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
}
|
||||
}
|
||||
Event::MouseButtonUp { mouse_btn, .. } => {
|
||||
if touch_debug {
|
||||
tracing::info!(?mouse_btn, "touch-debug: MouseButtonUp");
|
||||
touch_file_log!("MouseButtonUp mouse_btn={mouse_btn:?}");
|
||||
}
|
||||
if let Some(cap) = stream.as_mut().and_then(|s| s.capture.as_mut()) {
|
||||
cap.on_button_up(mouse_btn);
|
||||
}
|
||||
@@ -650,20 +597,6 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
timestamp,
|
||||
..
|
||||
} => {
|
||||
if touch_debug {
|
||||
tracing::info!(
|
||||
touch_id,
|
||||
finger_id,
|
||||
x,
|
||||
y,
|
||||
direct = is_direct_touch(touch_id),
|
||||
"touch-debug: FingerDown"
|
||||
);
|
||||
touch_file_log!(
|
||||
"FingerDown touch_id={touch_id} finger_id={finger_id} x={x} y={y} direct={}",
|
||||
is_direct_touch(touch_id)
|
||||
);
|
||||
}
|
||||
if is_direct_touch(touch_id)
|
||||
&& dispatch_finger(
|
||||
FingerPhase::Down,
|
||||
@@ -686,20 +619,6 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
timestamp,
|
||||
..
|
||||
} => {
|
||||
if touch_debug {
|
||||
tracing::info!(
|
||||
touch_id,
|
||||
finger_id,
|
||||
x,
|
||||
y,
|
||||
direct = is_direct_touch(touch_id),
|
||||
"touch-debug: FingerMotion"
|
||||
);
|
||||
touch_file_log!(
|
||||
"FingerMotion touch_id={touch_id} finger_id={finger_id} x={x} y={y} direct={}",
|
||||
is_direct_touch(touch_id)
|
||||
);
|
||||
}
|
||||
if is_direct_touch(touch_id)
|
||||
&& dispatch_finger(
|
||||
FingerPhase::Move,
|
||||
@@ -722,20 +641,6 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
timestamp,
|
||||
..
|
||||
} => {
|
||||
if touch_debug {
|
||||
tracing::info!(
|
||||
touch_id,
|
||||
finger_id,
|
||||
x,
|
||||
y,
|
||||
direct = is_direct_touch(touch_id),
|
||||
"touch-debug: FingerUp"
|
||||
);
|
||||
touch_file_log!(
|
||||
"FingerUp touch_id={touch_id} finger_id={finger_id} x={x} y={y} direct={}",
|
||||
is_direct_touch(touch_id)
|
||||
);
|
||||
}
|
||||
if is_direct_touch(touch_id)
|
||||
&& dispatch_finger(
|
||||
FingerPhase::Up,
|
||||
|
||||
@@ -885,9 +885,9 @@ pub const PUNKTFUNK_GAMEPAD_DUALSHOCK4: u32 = 4;
|
||||
/// UHID classic Steam Controller (Valve `28DE:1102`, kernel `hid-steam`): one stick + dual
|
||||
/// trackpads + two grip paddles. Honored only where available (Linux hosts); else Xbox 360.
|
||||
pub const PUNKTFUNK_GAMEPAD_STEAMCONTROLLER: u32 = 5;
|
||||
/// UHID Steam Deck controller (Valve `28DE:1205`, kernel `hid-steam`): full Deck gamepad incl. the
|
||||
/// four back grips, a right trackpad, and the IMU; re-grabbed by Steam Input with native glyphs when
|
||||
/// Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360.
|
||||
/// Steam Deck controller (Valve `28DE:1205`): full Deck gamepad incl. the four back grips, both
|
||||
/// trackpads, and the IMU; re-grabbed by Steam Input with native glyphs when Steam runs on the
|
||||
/// host. Honored on Linux AND Windows hosts; else folds to X-Box 360.
|
||||
pub const PUNKTFUNK_GAMEPAD_STEAMDECK: u32 = 6;
|
||||
/// DualSense Edge (Sony `054C:0DF2`): the DualSense plus two back buttons + two Fn buttons, so a
|
||||
/// client's back paddles land on native slots. Folds to `DUALSENSE` until its backend lands.
|
||||
|
||||
@@ -160,9 +160,10 @@ pub enum GamepadPref {
|
||||
/// trackpads + two grip paddles. The wire right stick drives the right pad; a left-pad contact
|
||||
/// shadows the stick (hardware multiplex). Needs Linux UHID.
|
||||
SteamController,
|
||||
/// UHID Steam Deck controller (Valve `28DE:1205`, kernel `hid-steam`) — full Deck gamepad incl.
|
||||
/// the four back grips (L4/L5/R4/R5), a right trackpad, and the IMU; re-grabbed by Steam Input
|
||||
/// with native glyphs when Steam runs on the host. Needs Linux UHID.
|
||||
/// Steam Deck controller (Valve `28DE:1205`) — full Deck gamepad incl. the four back grips
|
||||
/// (L4/L5/R4/R5), both trackpads, and the IMU; re-grabbed by Steam Input with native glyphs
|
||||
/// when Steam runs on the host. Linux (kernel `hid-steam` via UHID/usbip/gadget) or Windows
|
||||
/// (UMDF minidriver, Steam-Input-promoted).
|
||||
SteamDeck,
|
||||
/// DualSense Edge (Sony `054C:0DF2`, kernel `hid-playstation` ≥ 6.3 / Windows UMDF) — the
|
||||
/// DualSense plus two back buttons + two Fn buttons, so a client's back paddles (Deck grips,
|
||||
|
||||
@@ -473,6 +473,11 @@ fn gs_button_to_evdev(b: u32) -> Option<u32> {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/dualsense.rs"]
|
||||
pub mod dualsense;
|
||||
/// Windows: virtual DualSense **Edge** via the same UMDF minidriver + shared-memory channel
|
||||
/// (device-type 2) — the wire back grips land on the Edge's native back/Fn buttons.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/dualsense_edge_windows.rs"]
|
||||
pub mod dualsense_edge_windows;
|
||||
/// Transport-independent DualSense HID contract, shared by the Linux UHID backend ([`dualsense`])
|
||||
/// and the Windows UMDF-driver backend ([`dualsense_windows`]).
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
@@ -482,11 +487,6 @@ pub mod dualsense_proto;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/dualsense_windows.rs"]
|
||||
pub mod dualsense_windows;
|
||||
/// Windows: virtual DualSense **Edge** via the same UMDF minidriver + shared-memory channel
|
||||
/// (device-type 2) — the wire back grips land on the Edge's native back/Fn buttons.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/dualsense_edge_windows.rs"]
|
||||
pub mod dualsense_edge_windows;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/dualshock4.rs"]
|
||||
pub mod dualshock4;
|
||||
@@ -527,15 +527,11 @@ pub mod pad_slots;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/steam_controller.rs"]
|
||||
pub mod steam_controller;
|
||||
/// Linux: virtual Nintendo Switch Pro Controller via UHID (kernel `hid-nintendo`).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/switch_pro.rs"]
|
||||
pub mod switch_pro;
|
||||
/// Transport-independent Switch Pro Controller codec + the canned `hid-nintendo` handshake
|
||||
/// replies, used by the Linux UHID backend ([`switch_pro`]).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/proto/switch_proto.rs"]
|
||||
pub mod switch_proto;
|
||||
/// Windows: virtual Steam Deck via the same UMDF minidriver + shared-memory channel
|
||||
/// (device-type 3) — promoted by Steam Input thanks to the `&MI_02` hardware-id synthesis.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/steam_deck_windows.rs"]
|
||||
pub mod steam_deck_windows;
|
||||
/// Linux: virtual Steam Deck via the USB gadget subsystem (`raw_gadget` + `dummy_hcd`) — the only
|
||||
/// virtual-Deck transport Steam Input promotes (presents the controller on USB interface 2).
|
||||
/// SteamOS-host only (needs `dummy_hcd` + `raw_gadget`).
|
||||
@@ -543,8 +539,9 @@ pub mod switch_proto;
|
||||
#[path = "inject/linux/steam_gadget.rs"]
|
||||
pub mod steam_gadget;
|
||||
/// Transport-independent Steam Controller / Steam Deck HID contract (descriptor, byte-exact Deck
|
||||
/// serializer, XInput/rich mappers, rumble parser), used by the Linux UHID backend ([`steam_controller`]).
|
||||
#[cfg(target_os = "linux")]
|
||||
/// serializer, XInput/rich mappers, rumble parser), used by the Linux UHID backend
|
||||
/// ([`steam_controller`]) and the Windows UMDF backend ([`steam_deck_windows`]).
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
#[path = "inject/proto/steam_proto.rs"]
|
||||
pub mod steam_proto;
|
||||
/// Pure fallback-remap policy (Steam-only inputs onto a non-Steam backend) + the Deck motion rescale.
|
||||
@@ -559,6 +556,15 @@ pub mod steam_remap;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/steam_usbip.rs"]
|
||||
pub mod steam_usbip;
|
||||
/// Linux: virtual Nintendo Switch Pro Controller via UHID (kernel `hid-nintendo`).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/switch_pro.rs"]
|
||||
pub mod switch_pro;
|
||||
/// Transport-independent Switch Pro Controller codec + the canned `hid-nintendo` handshake
|
||||
/// replies, used by the Linux UHID backend ([`switch_pro`]).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/proto/switch_proto.rs"]
|
||||
pub mod switch_proto;
|
||||
/// The generic stateful virtual-pad manager ([`uhid_manager::UhidManager`]) — event routing, frame
|
||||
/// merge, heartbeat, and feedback pump shared by the five UHID/UMDF backends; each supplies only
|
||||
/// its per-controller protocol via [`uhid_manager::PadProto`] (G12).
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
//! UMDF-driver backend; this module is just the `/dev/uhid` plumbing around it.
|
||||
|
||||
use super::dualsense_proto::{
|
||||
edge_paddle_bits, parse_ds_output, serialize_state, DsFeedback, DsState,
|
||||
DS_EDGE_PRODUCT, DS_FEATURE_CALIBRATION, DS_FEATURE_FIRMWARE, DS_FEATURE_PAIRING,
|
||||
DS_INPUT_REPORT_LEN, DS_PRODUCT, DS_TOUCH_H, DS_TOUCH_W, DS_VENDOR, DUALSENSE_EDGE_RDESC,
|
||||
DUALSENSE_RDESC,
|
||||
edge_paddle_bits, parse_ds_output, serialize_state, DsFeedback, DsState, DS_EDGE_PRODUCT,
|
||||
DS_FEATURE_CALIBRATION, DS_FEATURE_FIRMWARE, DS_FEATURE_PAIRING, DS_INPUT_REPORT_LEN,
|
||||
DS_PRODUCT, DS_TOUCH_H, DS_TOUCH_W, DS_VENDOR, DUALSENSE_EDGE_RDESC, DUALSENSE_RDESC,
|
||||
};
|
||||
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||
use anyhow::{Context, Result};
|
||||
@@ -102,7 +101,8 @@ impl DualSensePad {
|
||||
format!("open {UHID_PATH} (is the 60-punktfunk.rules uhid rule installed + are you in 'input'?)")
|
||||
})?;
|
||||
let mut ds = DualSensePad { fd, seq: 0, ts: 0 };
|
||||
ds.send_create2(index, id).context("UHID_CREATE2 DualSense")?;
|
||||
ds.send_create2(index, id)
|
||||
.context("UHID_CREATE2 DualSense")?;
|
||||
Ok(ds)
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,12 @@ impl SwitchProPad {
|
||||
let mut ev = [0u8; UHID_EVENT_SIZE];
|
||||
ev[0..4].copy_from_slice(&UHID_CREATE2.to_ne_bytes());
|
||||
// union (uhid_create2_req) starts at byte 4.
|
||||
put_cstr(&mut ev, 4, 128, &format!("Punktfunk Switch Pro Controller {index}")); // name[128]
|
||||
put_cstr(
|
||||
&mut ev,
|
||||
4,
|
||||
128,
|
||||
&format!("Punktfunk Switch Pro Controller {index}"),
|
||||
); // name[128]
|
||||
put_cstr(&mut ev, 132, 64, &format!("punktfunk/switchpro/{index}")); // phys[64]
|
||||
put_cstr(&mut ev, 196, 64, &format!("punktfunk-swpro-{index}")); // uniq[64]
|
||||
ev[260..262].copy_from_slice(&(PROCON_RDESC.len() as u16).to_ne_bytes()); // rd_size
|
||||
@@ -123,7 +128,13 @@ impl SwitchProPad {
|
||||
let reply = match id {
|
||||
// Device info — the fatal one (probe aborts without it): type = Pro Controller +
|
||||
// this pad's virtual MAC. Real hardware acks it with 0x82.
|
||||
0x02 => build_subcmd_reply(&st, self.timer, 0x82, id, &device_info_payload(&switch_mac(self.index))),
|
||||
0x02 => build_subcmd_reply(
|
||||
&st,
|
||||
self.timer,
|
||||
0x82,
|
||||
id,
|
||||
&device_info_payload(&switch_mac(self.index)),
|
||||
),
|
||||
// SPI flash read: echoed addr + len + the canned calibration bytes. An unmapped
|
||||
// range answers zeroes (echoed header, zero data) — the driver then warns and uses
|
||||
// its defaults instead of stalling through 2 × 1 s timeouts.
|
||||
@@ -134,7 +145,11 @@ impl SwitchProPad {
|
||||
.unwrap_or(0);
|
||||
let len = args.get(4).copied().unwrap_or(0);
|
||||
let payload = spi_flash_read(addr, len).unwrap_or_else(|| {
|
||||
tracing::debug!(addr = format!("{addr:#x}"), len, "unmapped SPI read — zero fill");
|
||||
tracing::debug!(
|
||||
addr = format!("{addr:#x}"),
|
||||
len,
|
||||
"unmapped SPI read — zero fill"
|
||||
);
|
||||
let mut p = Vec::with_capacity(5 + len as usize);
|
||||
p.extend_from_slice(&addr.to_le_bytes());
|
||||
p.push(len);
|
||||
|
||||
@@ -824,11 +824,7 @@ mod tests {
|
||||
fn sc_serialize_and_mapping() {
|
||||
// Full mapping: face + grips + clicks + a deflected right stick.
|
||||
let s = sc_from_gamepad(
|
||||
gs::BTN_A
|
||||
| gs::BTN_PADDLE1
|
||||
| gs::BTN_PADDLE2
|
||||
| gs::BTN_LS_CLICK
|
||||
| gs::BTN_RS_CLICK,
|
||||
gs::BTN_A | gs::BTN_PADDLE1 | gs::BTN_PADDLE2 | gs::BTN_LS_CLICK | gs::BTN_RS_CLICK,
|
||||
1000,
|
||||
-2000,
|
||||
3000,
|
||||
|
||||
@@ -88,6 +88,11 @@ pub(super) struct SwDeviceProfile<'a> {
|
||||
pub hwid: &'a str,
|
||||
/// The USB VID&PID token (`VID_054C&PID_0CE6`) used to synthesize the USB hardware/compatible ids.
|
||||
pub usb_vid_pid: &'a str,
|
||||
/// USB composite interface number to synthesize (`&MI_xx` appended to the USB hardware ids).
|
||||
/// hidclass mirrors the parent's `USB\VID…` tokens into the HID child's hardware ids, and
|
||||
/// hidapi/SDL/Steam parse the child's `MI_` token as `bInterfaceNumber` (defaulting to 0 when
|
||||
/// absent) — the Steam Deck's controller lives on interface 2, the gate the N4 spike hit.
|
||||
pub usb_mi: Option<u8>,
|
||||
/// Device description shown in Device Manager.
|
||||
pub description: &'a str,
|
||||
}
|
||||
@@ -126,8 +131,9 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
};
|
||||
let usb_rev = format!("USB\\{}&REV_0100", p.usb_vid_pid);
|
||||
let usb = format!("USB\\{}", p.usb_vid_pid);
|
||||
let mi = p.usb_mi.map(|n| format!("&MI_{n:02}")).unwrap_or_default();
|
||||
let usb_rev = format!("USB\\{}&REV_0100{mi}", p.usb_vid_pid);
|
||||
let usb = format!("USB\\{}{mi}", p.usb_vid_pid);
|
||||
let hwids = multi_sz(&[
|
||||
p.hwid, // FIRST → the INF binds our UMDF driver on this id
|
||||
usb_rev.as_str(),
|
||||
@@ -297,6 +303,7 @@ impl DsWinPad {
|
||||
container_index: index,
|
||||
hwid: id.hwid,
|
||||
usb_vid_pid: id.usb_vid_pid,
|
||||
usb_mi: None, // single-interface USB devices (real DS/Edge have no MI_ token)
|
||||
description: id.description,
|
||||
}) {
|
||||
Ok((h, i)) => (Some(h), i),
|
||||
@@ -487,7 +494,7 @@ pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range. Device-type
|
||||
// FIRST, magic LAST — the same publish order the session pads use.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_STEAMDECK_SPIKE;
|
||||
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_STEAMDECK;
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; 64], neutral);
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
@@ -498,6 +505,9 @@ pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
|
||||
container_index: index,
|
||||
hwid: "pf_steamdeck",
|
||||
usb_vid_pid: "VID_28DE&PID_1205",
|
||||
// The Deck's controller interface — the promotion gate the first spike run hit
|
||||
// (hidapi parses MI_ from the child hwids; absent = interface 0, Steam wants 2).
|
||||
usb_mi: Some(2),
|
||||
description: "punktfunk Virtual Steam Deck (spike)",
|
||||
})?;
|
||||
let _sw = super::gamepad_raii::SwDevice::new(hsw);
|
||||
@@ -515,9 +525,8 @@ pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
|
||||
channel.pump();
|
||||
// Log any feature/output traffic Steam sends — each one is spike evidence.
|
||||
// SAFETY: base points at SHM_SIZE bytes; OFF_OUT_SEQ is in range.
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
let seq =
|
||||
unsafe { std::ptr::read_unaligned(channel.data_base().add(OFF_OUT_SEQ) as *const u32) };
|
||||
if seq != last_out_seq {
|
||||
last_out_seq = seq;
|
||||
let mut out = [0u8; 16];
|
||||
|
||||
@@ -64,6 +64,7 @@ impl Ds4WinPad {
|
||||
container_index: index,
|
||||
hwid: "pf_dualshock4",
|
||||
usb_vid_pid: "VID_054C&PID_09CC",
|
||||
usb_mi: None,
|
||||
description: "punktfunk Virtual DualShock 4",
|
||||
}) {
|
||||
Ok((h, id)) => (Some(h), id),
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
//! Virtual Steam Deck controller on Windows via the UMDF minidriver — the Windows analogue of
|
||||
//! the Linux UHID Deck ([`super::steam_controller`]'s `SteamProto`), sharing its whole codec
|
||||
//! ([`super::steam_proto`]: the byte-exact `ID_CONTROLLER_DECK_STATE` serializer, the
|
||||
//! `XInput`/rich mappers, the `0xEB` rumble parser).
|
||||
//!
|
||||
//! Transport = the sealed shared-memory channel + a `SwDeviceCreate` devnode (device-type 3),
|
||||
//! like the PS pads — with the promotion lever the N4 spike proved: the synthesized USB
|
||||
//! hardware ids carry **`&MI_02`** (the Deck's wired controller interface), which hidclass
|
||||
//! mirrors into the HID child and hidapi/Steam parse as `bInterfaceNumber`. Steam Input then
|
||||
//! claims the pad exactly like a physical wired Deck (`!! Steam controller device opened`,
|
||||
//! XInput slot reserved — observed live on `.173`), so games get native Deck glyphs +
|
||||
//! trackpads + gyro + back grips through Steam's own remapping.
|
||||
//!
|
||||
//! Feedback: Steam drives Deck rumble (`0xEB`) and trackpad haptic pulses (`0x8F`) via
|
||||
//! SET_FEATURE on the unnumbered report; the driver republishes those into the section's
|
||||
//! output slot (report-id-0 prefixed), where [`parse_steam_output`] reads the exact wire shape
|
||||
//! the Linux path sees. No gamepad-mode entry pulse here — that gate lives in the Linux
|
||||
//! kernel's evdev parser; Steam-on-Windows reads the raw reports directly.
|
||||
|
||||
use super::dualsense_windows::{
|
||||
create_swdevice, SwDeviceProfile, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT, OFF_OUTPUT,
|
||||
OFF_OUT_SEQ, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE,
|
||||
};
|
||||
use super::gamepad_raii::PadChannel;
|
||||
use super::steam_proto::{
|
||||
neutral_deck_report, parse_steam_output, serialize_deck_state, SteamState, STEAM_REPORT_LEN,
|
||||
};
|
||||
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||
use anyhow::Result;
|
||||
use punktfunk_core::quic::RichInput;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A single virtual Steam Deck: the `SwDeviceCreate`'d `pf_deck_<index>` devnode plus the sealed
|
||||
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
|
||||
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait).
|
||||
pub struct DeckWinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds (RAII — `SwDeviceClose` on drop).
|
||||
_sw: Option<super::gamepad_raii::SwDevice>,
|
||||
/// The sealed channel: unnamed DATA section (`PadShm`) + bootstrap mailbox + handle delivery.
|
||||
channel: PadChannel,
|
||||
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
|
||||
attach: super::gamepad_raii::DriverAttach,
|
||||
seq: u32,
|
||||
last_out_seq: u32,
|
||||
}
|
||||
|
||||
impl DeckWinPad {
|
||||
/// Create the sealed channel, stamp `device_type = Steam Deck` FIRST + the pad index + the
|
||||
/// neutral Deck frame + the magic LAST, then spawn the `pf_deck_<index>` devnode with the
|
||||
/// `MI_02` USB identity Steam's promotion gate requires.
|
||||
fn open(index: u8) -> Result<DeckWinPad> {
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_STEAMDECK;
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(
|
||||
base.add(OFF_INPUT) as *mut [u8; STEAM_REPORT_LEN],
|
||||
neutral_deck_report(),
|
||||
);
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
let inst = format!("pf_deck_{index}");
|
||||
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
container_index: index,
|
||||
hwid: "pf_steamdeck",
|
||||
usb_vid_pid: "VID_28DE&PID_1205",
|
||||
// The wired Deck controller interface — WITHOUT this the HID child carries no MI_
|
||||
// token, hidapi reports interface 0, and Steam never claims the pad (the N4
|
||||
// spike's run-1 failure).
|
||||
usb_mi: Some(2),
|
||||
description: "punktfunk Virtual Steam Deck",
|
||||
}) {
|
||||
Ok((h, i)) => (Some(h), i),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; Steam Deck devnode unavailable");
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
|
||||
// Bounded eager delivery — the driver must read `device_type = 3` before hidclass asks
|
||||
// it for descriptors, or the pad would enumerate with the default DualSense identity.
|
||||
channel.deliver_eager(Duration::from_millis(1500));
|
||||
Ok(DeckWinPad {
|
||||
_sw,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_steamdeck",
|
||||
"pf_dualsense.inf", // one driver package serves every identity
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
boot_name,
|
||||
instance_id,
|
||||
),
|
||||
seq: 0,
|
||||
last_out_seq: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialize `st` into the Deck state frame and publish it to the section's input slot.
|
||||
fn write_state(&mut self, st: &SteamState) {
|
||||
self.seq = self.seq.wrapping_add(1);
|
||||
let mut r = [0u8; STEAM_REPORT_LEN];
|
||||
serialize_deck_state(&mut r, st, self.seq);
|
||||
// SAFETY: base points at SHM_SIZE bytes; input slot is OFF_INPUT..OFF_INPUT+64.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
r.as_ptr(),
|
||||
self.channel.data_base().add(OFF_INPUT),
|
||||
r.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Poll the section's output slot; parse a newly-published Steam command (`0xEB` rumble /
|
||||
/// `0x8F` haptic pulse — republished by the driver off SET_FEATURE) into feedback. Also
|
||||
/// ticks the sealed-channel delivery and the driver-attach health watcher.
|
||||
fn service(&mut self) -> Option<(u16, u16)> {
|
||||
self.channel.pump();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let proto = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_DRIVER_PROTO) as *const u32)
|
||||
};
|
||||
self.attach.observe(proto);
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
let seq = unsafe {
|
||||
std::ptr::read_unaligned(self.channel.data_base().add(OFF_OUT_SEQ) as *const u32)
|
||||
};
|
||||
if seq == self.last_out_seq {
|
||||
return None;
|
||||
}
|
||||
self.last_out_seq = seq;
|
||||
let mut out = [0u8; 64];
|
||||
// SAFETY: output slot is OFF_OUTPUT..OFF_OUTPUT+64 within the section.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
self.channel.data_base().add(OFF_OUTPUT),
|
||||
out.as_mut_ptr(),
|
||||
64,
|
||||
)
|
||||
};
|
||||
parse_steam_output(&out).rumble
|
||||
}
|
||||
}
|
||||
|
||||
/// The Windows-Deck half of the shared stateful manager (see [`PadProto`]): the sealed-channel
|
||||
/// open under the promoted Deck identity, the same [`SteamState`] mappers as the Linux backend,
|
||||
/// and the section feedback poll. Lifecycle (slot table, unplug sweep, heartbeat, rumble dedup)
|
||||
/// lives in [`UhidManager`].
|
||||
#[derive(Default)]
|
||||
pub struct DeckWinProto;
|
||||
|
||||
impl PadProto for DeckWinProto {
|
||||
type Pad = DeckWinPad;
|
||||
type State = SteamState;
|
||||
const LABEL: &'static str = "Steam Deck/Windows";
|
||||
const DEVICE: &'static str = "Steam Deck";
|
||||
const CREATE_HINT: &'static str =
|
||||
" (install/repair: punktfunk-host.exe driver install --gamepad)";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<DeckWinPad> {
|
||||
let p = DeckWinPad::open(idx)?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual Steam Deck created (Windows UMDF shm channel, MI_02 promoted identity)"
|
||||
);
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn neutral(&self) -> SteamState {
|
||||
SteamState::neutral()
|
||||
}
|
||||
|
||||
/// Merge buttons/sticks/triggers, preserving the rich-plane fields (trackpads + motion +
|
||||
/// pad clicks arrive separately and must survive a button-only frame) — identical to the
|
||||
/// Linux `SteamProto::merge_frame`.
|
||||
fn merge_frame(
|
||||
&self,
|
||||
prev: &SteamState,
|
||||
f: &crate::gamestream::gamepad::GamepadFrame,
|
||||
) -> SteamState {
|
||||
use super::steam_proto::btn;
|
||||
let mut s = SteamState::from_gamepad(
|
||||
f.buttons,
|
||||
f.ls_x,
|
||||
f.ls_y,
|
||||
f.rs_x,
|
||||
f.rs_y,
|
||||
f.left_trigger,
|
||||
f.right_trigger,
|
||||
);
|
||||
s.rpad_x = prev.rpad_x;
|
||||
s.rpad_y = prev.rpad_y;
|
||||
s.lpad_x = prev.lpad_x;
|
||||
s.lpad_y = prev.lpad_y;
|
||||
s.gyro = prev.gyro;
|
||||
s.accel = prev.accel;
|
||||
s.buttons |= prev.buttons & (btn::RPAD_TOUCH | btn::LPAD_TOUCH);
|
||||
s.lpad_click = prev.lpad_click;
|
||||
s.rpad_click = prev.rpad_click;
|
||||
s
|
||||
}
|
||||
|
||||
fn apply_rich(&self, st: &mut SteamState, rich: RichInput) {
|
||||
st.apply_rich(rich);
|
||||
}
|
||||
|
||||
fn write_state(&self, pad: &mut DeckWinPad, st: &SteamState) {
|
||||
pad.write_state(st);
|
||||
}
|
||||
|
||||
/// Poll the section for Steam's feedback: motor rumble on the universal 0xCA plane. The
|
||||
/// Deck has no rich host→client feedback plane (no lightbar / adaptive triggers), so
|
||||
/// `hidout` stays empty — parity with the Linux backend.
|
||||
fn service(&self, pad: &mut DeckWinPad, _idx: u8) -> PadFeedback {
|
||||
PadFeedback {
|
||||
rumble: pad.service(),
|
||||
hidout: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All virtual Steam Deck pads of a Windows session — the analogue of the Linux
|
||||
/// `SteamControllerManager`, with the same method surface (via the shared [`UhidManager`]) as
|
||||
/// the other Windows pad managers.
|
||||
pub type SteamDeckWindowsManager = UhidManager<DeckWinProto>;
|
||||
@@ -429,10 +429,13 @@ fn real_main() -> Result<()> {
|
||||
let xbox = args.iter().any(|a| a == "--xbox");
|
||||
// `--edge` drives the DualSense Edge backend (device_type 2) and additionally holds
|
||||
// the R4/L4 paddles on the pressed beats, so a HID read shows the Edge bits in
|
||||
// report byte 10 (0x80|0x40) next to Cross.
|
||||
// report byte 10 (0x80|0x40) next to Cross. `--deck` drives the Steam Deck backend
|
||||
// (device_type 3, the MI_02-promoted identity) — watch Steam claim it live.
|
||||
let edge = args.iter().any(|a| a == "--edge");
|
||||
let extra_buttons: u32 = if edge {
|
||||
punktfunk_core::input::gamepad::BTN_PADDLE1 | punktfunk_core::input::gamepad::BTN_PADDLE2
|
||||
let deck = args.iter().any(|a| a == "--deck");
|
||||
let extra_buttons: u32 = if edge || deck {
|
||||
punktfunk_core::input::gamepad::BTN_PADDLE1
|
||||
| punktfunk_core::input::gamepad::BTN_PADDLE2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -534,6 +537,11 @@ fn real_main() -> Result<()> {
|
||||
inject::dualsense_edge_windows::DualSenseEdgeWindowsManager::new(),
|
||||
"DualSense Edge"
|
||||
);
|
||||
} else if deck {
|
||||
drive!(
|
||||
inject::steam_deck_windows::SteamDeckWindowsManager::new(),
|
||||
"Steam Deck"
|
||||
);
|
||||
} else {
|
||||
drive!(
|
||||
inject::dualsense_windows::DualSenseWindowsManager::new(),
|
||||
|
||||
@@ -1754,7 +1754,8 @@ const INJECTOR_REOPEN_BACKOFF: std::time::Duration = std::time::Duration::from_s
|
||||
/// two identities), the XUSB companion driver (classic XInput) on Windows.
|
||||
/// - DualSense / DualSense Edge / DualShock 4 — Linux UHID `hid-playstation`, or the Windows UMDF
|
||||
/// minidriver (device-type 0/2/1).
|
||||
/// - Steam Deck — Linux UHID `hid-steam`.
|
||||
/// - Steam Deck — Linux UHID `hid-steam` (or usbip/gadget), or the Windows UMDF minidriver
|
||||
/// (device-type 3, Steam-Input-promoted).
|
||||
///
|
||||
/// [`resolve_pad_kind`] folds any kind a platform can't build into one it can, so this never
|
||||
/// constructs a manager the build lacks.
|
||||
@@ -1787,6 +1788,8 @@ struct Pads {
|
||||
dualsense_edge_win: Option<crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualshock4_win: Option<crate::inject::dualshock4_windows::DualShock4WindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
steamdeck_win: Option<crate::inject::steam_deck_windows::SteamDeckWindowsManager>,
|
||||
}
|
||||
|
||||
impl Pads {
|
||||
@@ -1822,6 +1825,8 @@ impl Pads {
|
||||
dualsense_edge_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualshock4_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
steamdeck_win: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1924,6 +1929,11 @@ impl Pads {
|
||||
crate::inject::dualshock4_windows::DualShock4WindowsManager::new,
|
||||
)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::SteamDeck => self
|
||||
.steamdeck_win
|
||||
.get_or_insert_with(crate::inject::steam_deck_windows::SteamDeckWindowsManager::new)
|
||||
.handle(ev),
|
||||
_ => self
|
||||
.xbox360
|
||||
.get_or_insert_with(crate::inject::gamepad::GamepadManager::new)
|
||||
@@ -2006,6 +2016,12 @@ impl Pads {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::SteamDeck => {
|
||||
if let Some(m) = &mut self.steamdeck_win {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2057,6 +2073,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualshock4_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.steamdeck_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2099,6 +2118,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualshock4_win {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.steamdeck_win {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2783,11 +2805,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
|
||||
// are the N4 spike).
|
||||
GamepadPref::SteamDeck if linux => GamepadPref::SteamDeck,
|
||||
GamepadPref::SteamController if linux => GamepadPref::SteamController,
|
||||
// No virtual Deck on Windows (M7) — fold to DualSense, the closest rich pad: its
|
||||
// backend keeps gyro + trackpads + pad-click alive (the Deck's dual pads split the
|
||||
// DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped
|
||||
// all of that silently.
|
||||
GamepadPref::SteamDeck if windows => GamepadPref::DualSense,
|
||||
// Windows virtual Deck: the UMDF device-type-3 identity, Steam-Input-promoted via the
|
||||
// MI_02 hardware-id synthesis (gamepad-new-types N4) — native Deck glyphs + trackpads +
|
||||
// gyro + back grips, replacing the old fold to DualSense.
|
||||
GamepadPref::SteamDeck if windows => GamepadPref::SteamDeck,
|
||||
// DualSense Edge: Linux UHID hid-playstation / Windows UMDF (device-type 2) — the plain
|
||||
// DualSense plus native back/Fn buttons, so the wire paddles stop hitting the fold/drop
|
||||
// policy. Degrades to Xbox360 elsewhere like its siblings.
|
||||
@@ -5373,11 +5394,11 @@ mod tests {
|
||||
assert_eq!(pick_gamepad(Auto, Some("series"), true, false), XboxOne);
|
||||
assert_eq!(pick_gamepad(XboxOne, None, false, true), Xbox360);
|
||||
|
||||
// Steam Deck: native on Linux; folds to DualSense on Windows (keeps gyro + trackpads
|
||||
// via the UMDF backend — Xbox360 would drop the whole rich plane); Xbox360 elsewhere.
|
||||
// Steam Deck: native on Linux (UHID/usbip/gadget) AND Windows (UMDF device-type 3,
|
||||
// Steam-Input-promoted via MI_02 — gamepad-new-types N4); Xbox360 elsewhere.
|
||||
assert_eq!(pick_gamepad(SteamDeck, None, true, false), SteamDeck);
|
||||
assert_eq!(pick_gamepad(SteamDeck, None, false, true), DualSense);
|
||||
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense);
|
||||
assert_eq!(pick_gamepad(SteamDeck, None, false, true), SteamDeck);
|
||||
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), SteamDeck);
|
||||
assert_eq!(pick_gamepad(SteamDeck, None, false, false), Xbox360);
|
||||
// Classic Steam Controller: native on Linux (UHID hid-steam); Xbox360 elsewhere.
|
||||
assert_eq!(
|
||||
@@ -5400,14 +5421,14 @@ mod tests {
|
||||
pick_gamepad(DualSenseEdge, None, false, true),
|
||||
DualSenseEdge
|
||||
);
|
||||
assert_eq!(
|
||||
pick_gamepad(Auto, Some("edge"), true, false),
|
||||
DualSenseEdge
|
||||
);
|
||||
assert_eq!(pick_gamepad(Auto, Some("edge"), true, false), DualSenseEdge);
|
||||
assert_eq!(pick_gamepad(DualSenseEdge, None, false, false), Xbox360);
|
||||
// Switch Pro: native on Linux (UHID hid-nintendo); Xbox360 on Windows and elsewhere.
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, true, false), SwitchPro);
|
||||
assert_eq!(pick_gamepad(Auto, Some("switchpro"), true, false), SwitchPro);
|
||||
assert_eq!(
|
||||
pick_gamepad(Auto, Some("switchpro"), true, false),
|
||||
SwitchPro
|
||||
);
|
||||
assert_eq!(pick_gamepad(Auto, Some("switch"), true, false), SwitchPro);
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, false, true), Xbox360);
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, false, false), Xbox360);
|
||||
|
||||
@@ -95,7 +95,7 @@ See your desktop page ([KDE](/docs/kde), [GNOME](/docs/gnome)) for when to set t
|
||||
|
||||
| Setting | Values | Meaning |
|
||||
|---|---|---|
|
||||
| `PUNKTFUNK_GAMEPAD` | `xbox360` · `xboxone` · `dualsense` · `dualsenseedge` · `dualshock4` · `steamdeck` · `switchpro` · `steamcontroller` (aliases: `ps5`, `edge`, `ps4`, `deck`, `switch`, …) | The virtual pad the host creates. Usually **auto-resolved from the client's physical controller** — set this only to force a type. `xbox360` (XInput) is the universal fallback. `dualsenseedge` gives the client's back paddles native buttons; `switchpro` gives Nintendo-family pads correct glyphs/layout + gyro. DualSense (Edge)/DualShock 4/Steam Deck/Switch Pro need Linux UHID; unsupported choices fold to Xbox 360. |
|
||||
| `PUNKTFUNK_GAMEPAD` | `xbox360` · `xboxone` · `dualsense` · `dualsenseedge` · `dualshock4` · `steamdeck` · `switchpro` · `steamcontroller` (aliases: `ps5`, `edge`, `ps4`, `deck`, `switch`, …) | The virtual pad the host creates. Usually **auto-resolved from the client's physical controller** — set this only to force a type. `xbox360` (XInput) is the universal fallback. `dualsenseedge` gives the client's back paddles native buttons; `switchpro` gives Nintendo-family pads correct glyphs/layout + gyro. DualSense (Edge)/DualShock 4 work on Linux (UHID) and Windows (UMDF); the Steam Deck pad too (Windows via the promoted UMDF identity); Switch Pro and the classic Steam Controller need Linux UHID. Unsupported choices fold to Xbox 360. |
|
||||
| `PUNKTFUNK_STEAM_GADGET` | `1` · `0` | Force the raw USB-gadget virtual Steam Deck on/off. **On by default on SteamOS**, off elsewhere. Lets Steam promote the virtual Deck to full Steam Input. |
|
||||
|
||||
## Audio / microphone
|
||||
|
||||
@@ -112,13 +112,13 @@
|
||||
// hosts); otherwise the host falls back to X-Box 360.
|
||||
#define PUNKTFUNK_GAMEPAD_DUALSHOCK4 4
|
||||
|
||||
// UHID classic Steam Controller (Valve `28DE:1102`, kernel `hid-steam`): dual trackpads, gyro,
|
||||
// two grip paddles. Reserved — currently folds to `XBOX360` until its backend lands.
|
||||
// UHID classic Steam Controller (Valve `28DE:1102`, kernel `hid-steam`): one stick + dual
|
||||
// trackpads + two grip paddles. Honored only where available (Linux hosts); else Xbox 360.
|
||||
#define PUNKTFUNK_GAMEPAD_STEAMCONTROLLER 5
|
||||
|
||||
// UHID Steam Deck controller (Valve `28DE:1205`, kernel `hid-steam`): full Deck gamepad incl. the
|
||||
// four back grips, a right trackpad, and the IMU; re-grabbed by Steam Input with native glyphs when
|
||||
// Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360.
|
||||
// Steam Deck controller (Valve `28DE:1205`): full Deck gamepad incl. the four back grips, both
|
||||
// trackpads, and the IMU; re-grabbed by Steam Input with native glyphs when Steam runs on the
|
||||
// host. Honored on Linux AND Windows hosts; else folds to X-Box 360.
|
||||
#define PUNKTFUNK_GAMEPAD_STEAMDECK 6
|
||||
|
||||
// DualSense Edge (Sony `054C:0DF2`): the DualSense plus two back buttons + two Fn buttons, so a
|
||||
|
||||
@@ -614,14 +614,15 @@ fn on_output_report(request: &Request, ioctl: ULONG) -> NTSTATUS {
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
/// N4 spike: the last SET_FEATURE payload (the Steam command byte + args, minus the report-id
|
||||
/// prefix). Steam's Deck contract is command-in-SET_FEATURE → answer-in-GET_FEATURE on the one
|
||||
/// unnumbered feature report; the PS identities ignore this (their SET_FEATUREs are fire-and-
|
||||
/// forget) — acking them is all they need.
|
||||
/// Deck identity: the last SET_FEATURE payload (the Steam command byte + args, minus the
|
||||
/// report-id prefix). Steam's Deck contract is command-in-SET_FEATURE → answer-in-GET_FEATURE
|
||||
/// on the one unnumbered feature report; the PS identities ignore this (their SET_FEATUREs are
|
||||
/// fire-and-forget) — acking them is all they need.
|
||||
static LAST_SET_FEATURE: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new([0; 64]);
|
||||
|
||||
// SET_FEATURE: ack (the PS identities' contract), and latch the payload for the Deck's
|
||||
// GET_FEATURE answer. Per the UMDF marshalling convention the report data is the input buffer.
|
||||
// SET_FEATURE: ack (the PS identities' contract), latch the payload for the Deck's GET_FEATURE
|
||||
// answer, and — the Deck feedback path — publish Steam's rumble/haptic commands to the host.
|
||||
// Per the UMDF marshalling convention the report data is the input buffer.
|
||||
fn on_set_feature(request: &Request) -> NTSTATUS {
|
||||
if let Ok((bytes, _)) = request.input_bytes(64) {
|
||||
// The wire carries [report-id 0, cmd, …] for the unnumbered Steam report; store the
|
||||
@@ -636,19 +637,44 @@ fn on_set_feature(request: &Request) -> NTSTATUS {
|
||||
let n = src.len().min(64);
|
||||
g[..n].copy_from_slice(&src[..n]);
|
||||
}
|
||||
// Deck feedback: Steam drives rumble (0xEB) and trackpad haptic pulses (0x8F) via
|
||||
// SET_FEATURE on the unnumbered report — the PS identities get theirs as OUTPUT
|
||||
// reports instead. Publish them to the host through the same output slot + seq the
|
||||
// output path uses, re-prefixed with the report-id 0 byte so the host's
|
||||
// `parse_steam_output` sees the exact wire shape the Linux UHID path delivers.
|
||||
if device_type() == 3
|
||||
&& matches!(src.first(), Some(&0xEB) | Some(&0x8F))
|
||||
&& let Some(view) = CHANNEL.data()
|
||||
{
|
||||
let mut out = [0u8; 64];
|
||||
let n = src.len().min(63);
|
||||
out[1..1 + n].copy_from_slice(&src[..n]);
|
||||
view.write_bytes(OFF_OUTPUT, &out);
|
||||
let seq = view.read_u32(OFF_OUT_SEQ).wrapping_add(1);
|
||||
view.write_u32(OFF_OUT_SEQ, seq);
|
||||
}
|
||||
}
|
||||
dbglog!("[pf-ds] SET_FEATURE (acked, latched for GET)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
/// N4 spike: build the Deck's GET_FEATURE reply from the latched SET_FEATURE command — the
|
||||
/// Deck identity: build the GET_FEATURE reply from the latched SET_FEATURE command — the
|
||||
/// 0x83 GET_ATTRIBUTES 9-attribute blob (unit id keyed per pad) or the 0xAE unit serial, both
|
||||
/// captured from a physical Deck (see inject/proto/steam_proto.rs feature_reply, the source of
|
||||
/// truth this mirrors). Anything else echoes the latched command.
|
||||
fn deck_feature_reply() -> [u8; 64] {
|
||||
let last = LAST_SET_FEATURE.lock().map(|g| *g).unwrap_or([0u8; 64]);
|
||||
let unit_id: u32 = 0x5046_0003; // "PF" + the spike's scratch index
|
||||
let serial = b"PFDK50460003";
|
||||
// Per-pad unit id "PF" + the pad index the host stamped into the section (0 while the
|
||||
// channel hasn't attached yet) — matches steam_proto::deck_unit_id / deck_serial, so two
|
||||
// virtual Decks never collide in Steam's eyes.
|
||||
let idx = CHANNEL
|
||||
.data()
|
||||
.map(|v| v.read_u32(OFF_PAD_INDEX))
|
||||
.unwrap_or(0)
|
||||
& 0xFF;
|
||||
let unit_id: u32 = 0x5046_0000 | idx;
|
||||
let serial = format!("PFDK{unit_id:08X}");
|
||||
let serial = serial.as_bytes();
|
||||
let mut r = [0u8; 64];
|
||||
match last[0] {
|
||||
0x83 => {
|
||||
@@ -736,23 +762,32 @@ fn on_get_string(request: &Request) -> NTSTATUS {
|
||||
let string_id = id_val & 0xFFFF;
|
||||
let devtype = device_type();
|
||||
dbglog!("[pf-ds] GET_STRING id=0x{string_id:04x} (raw 0x{id_val:08x}) devtype={devtype}");
|
||||
let s: &str = match string_id {
|
||||
let s: String = match string_id {
|
||||
0 | 0x000e => match devtype {
|
||||
1 => "Sony Computer Entertainment",
|
||||
3 => "Valve Software",
|
||||
_ => "Sony Interactive Entertainment",
|
||||
1 => "Sony Computer Entertainment".into(),
|
||||
3 => "Valve Software".into(),
|
||||
_ => "Sony Interactive Entertainment".into(),
|
||||
},
|
||||
2 | 0x0010 => match devtype {
|
||||
1 => "DEADBEEF0001",
|
||||
2 => "35533AD6E775",
|
||||
3 => "PFDK50460003",
|
||||
_ => "35533AD6E774",
|
||||
1 => "DEADBEEF0001".into(),
|
||||
2 => "35533AD6E775".into(),
|
||||
// Per-pad Deck serial — must agree with deck_feature_reply's 0xAE answer (Steam
|
||||
// reads both and uses the serial to identify units).
|
||||
3 => {
|
||||
let idx = CHANNEL
|
||||
.data()
|
||||
.map(|v| v.read_u32(OFF_PAD_INDEX))
|
||||
.unwrap_or(0)
|
||||
& 0xFF;
|
||||
format!("PFDK{:08X}", 0x5046_0000u32 | idx)
|
||||
}
|
||||
_ => "35533AD6E774".into(),
|
||||
},
|
||||
_ => match devtype {
|
||||
1 => "Wireless Controller",
|
||||
2 => "DualSense Edge Wireless Controller",
|
||||
3 => "Steam Deck Controller",
|
||||
_ => "DualSense Wireless Controller",
|
||||
1 => "Wireless Controller".into(),
|
||||
2 => "DualSense Edge Wireless Controller".into(),
|
||||
3 => "Steam Deck Controller".into(),
|
||||
_ => "DualSense Wireless Controller".into(),
|
||||
},
|
||||
};
|
||||
let mut wide: Vec<u8> = Vec::with_capacity(s.len() * 2 + 2);
|
||||
@@ -764,7 +799,8 @@ fn on_get_string(request: &Request) -> NTSTATUS {
|
||||
}
|
||||
|
||||
/// The host's device-type selector from the sealed DATA section (`device_type` @140): 0 = DualSense
|
||||
/// (default), 1 = DualShock 4, 2 = DualSense Edge. Read fresh on each enumeration query — cheap. If
|
||||
/// (default), 1 = DualShock 4, 2 = DualSense Edge, 3 = Steam Deck. 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/Edge pad must not enumerate with the default DualSense
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Repo pre-commit hook: the same rustfmt gates as pre-push, but at commit time for faster
|
||||
# feedback. Enable once per clone:
|
||||
#
|
||||
# git config core.hooksPath scripts/git-hooks
|
||||
#
|
||||
# NOTE this is the convenience layer only — plumbing commits (commit-tree) bypass pre-commit,
|
||||
# which is why pre-push re-runs the identical checks and is the actual enforcement point.
|
||||
exec "$(git rev-parse --show-toplevel)/scripts/git-hooks/pre-push"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# Repo pre-push hook: run the exact rustfmt gates CI runs, so a push can never fail on
|
||||
# formatting alone. Enable once per clone:
|
||||
#
|
||||
# git config core.hooksPath scripts/git-hooks
|
||||
#
|
||||
# Kept to the CHEAP checks on purpose (rustfmt only parses — a couple of seconds): clippy/tests
|
||||
# stay in CI and the per-box verify recipes. Runs regardless of how the commits were made
|
||||
# (plain `git commit`, amends, or plumbing like commit-tree — pre-push sees them all).
|
||||
#
|
||||
# Escape hatch for a genuine emergency: `git push --no-verify`.
|
||||
|
||||
set -eu
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
|
||||
fail() {
|
||||
echo "pre-push: $1" >&2
|
||||
echo "pre-push: fix with: $2" >&2
|
||||
echo "pre-push: (bypass in an emergency with: git push --no-verify)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Main workspace — mirrors ci.yml "Format": cargo fmt --all --check.
|
||||
if ! (cd "$root" && cargo fmt --all --check >/dev/null 2>&1); then
|
||||
fail "main workspace is not rustfmt-clean (ci.yml would fail)" \
|
||||
"cargo fmt --all"
|
||||
fi
|
||||
|
||||
# 2. UMDF driver workspace — mirrors windows-drivers.yml's gate (the shipped drivers + the
|
||||
# audited safe layer; pf-vdisplay is deliberately NOT gated there, so not here either).
|
||||
if ! (cd "$root/packaging/windows/drivers" \
|
||||
&& cargo fmt -p pf-umdf-util -p pf-xusb -p pf-dualsense --check >/dev/null 2>&1); then
|
||||
fail "driver workspace is not rustfmt-clean (windows-drivers.yml would fail)" \
|
||||
"(cd packaging/windows/drivers && cargo fmt -p pf-umdf-util -p pf-xusb -p pf-dualsense)"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user