5 Commits

Author SHA1 Message Date
enricobuehler 90c2d8b3a0 fix(host): don't count punktfunk's own virtual Deck as a physical Steam controller
apple / swift (push) Successful in 1m7s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / web (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
The Steam-conflict gate scanned /sys/bus/hid/devices for non-virtual 28DE
devices, but the usbip/gadget virtual Decks present a REAL USB device (vhci
resolves through vhci_hcd, not /devices/virtual/) — so a just-ended session's
pad still detaching, or a concurrent session's live one, read as "physical
Steam controller attached" and degraded every back-to-back Deck session to
DualSense (observed live on Bazzite). Exclude our pads by their PFDK… serial
(HID_UNIQ), with the vhci_hcd path as belt and braces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:14:24 +00:00
enricobuehler 853e7fe92f fix(client-linux): Deck trackpad clicks — bind to the correct pad, stop riding the button plane
apple / swift (push) Successful in 1m6s
ci / rust (push) Successful in 1m27s
ci / web (push) Successful in 50s
android / android (push) Successful in 3m19s
ci / docs-site (push) Successful in 1m6s
apple / screenshots (push) Successful in 5m29s
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
SDL's Steam Deck mapping delivers the pad clicks as gamepad BUTTONS with no
surface identity: the generic `touchpad` button is the LEFT pad's click and
`misc2` the RIGHT's (SDL_gamepad_db.h `touchpad:b17,misc2:b16`). The client
forwarded `touchpad` as wire BTN_TOUCHPAD — which the host maps to the RIGHT
pad click (DualSense convention) — and dropped `misc2` entirely: a left-pad
click registered on the right pad, a right-pad click nowhere, and the
mis-routed state could stick.

Clicks from a multi-touchpad pad now ride the rich plane as TouchpadEx with
their surface, reusing the surface's live contact point (click buttons carry
no position). forward_touch carries the held click through motion frames so a
touch update can't clear a click mid-press, and the flush lifts held clicks on
detach/pad-switch. A DualSense's single touchpad button stays on the button
plane unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:08:44 +00:00
enricobuehler df496776b0 fix(client-linux): Deck raw-pad capture — clear Steam's SDL device filter, honest degradation warning
apple / swift (push) Successful in 1m14s
apple / screenshots (push) Successful in 5m41s
android / android (push) Successful in 3m46s
ci / web (push) Successful in 49s
ci / rust (push) Successful in 1m23s
ci / docs-site (push) Successful in 58s
ci / bench (push) Successful in 4m52s
deb / build-publish (push) Successful in 4m34s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Successful in 4m29s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m58s
docker / deploy-docs (push) Successful in 6s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m8s
The Deck's built-in controller can never leave Steam Input ("Steam Controller"
is always-required in the shortcut's matrix; Disable Steam Input only affects
other controller brands), so the raw 28DE:1205 device is the only path to the
trackpads/paddles/gyro. Steam hides it from SDL by launching shortcuts with
SDL_GAMECONTROLLER_IGNORE_DEVICES naming every physical pad it virtualized —
clear it (and _EXCEPT) at startup while single-threaded, logging what Steam set
as field evidence. The post-attach warning now states the real condition (raw
pad never enumerated; sticks + buttons still work) instead of advising a
Steam Input toggle that doesn't exist for the built-in controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 10:06:48 +00:00
enricobuehler 5310176ab5 fix(client-linux,host): Deck video defaults to software decode + input-interception diagnostics
apple / swift (push) Successful in 1m8s
apple / screenshots (push) Successful in 5m38s
windows-host / package (push) Successful in 7m12s
android / android (push) Successful in 3m36s
ci / rust (push) Successful in 1m31s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 57s
ci / bench (push) Successful in 4m56s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
deb / build-publish (push) Successful in 4m38s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
flatpak / build-publish (push) Successful in 4m55s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m57s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m21s
docker / deploy-docs (push) Successful in 17s
Video (Deck): the VAAPI zero-copy path renders corrupt/gray/washed-out on the
Deck — root-caused to Mesa >= 25.1 exporting radeonsi VCN decode surfaces TILED
(the Flatpak runtime's Mesa 26 drives both the decoder and GTK's GL, and GTK's
tiled-NV12 dmabuf import mishandles it; desktop Tier-1 validations ran distro
Mesa with linear export). `auto` now resolves to software on a Deck (clean,
correct-colour, easily handles 1280x800 HEVC); PUNKTFUNK_DECODER=vaapi still
forces the hw path, with the descriptor modifier dump + GSK_RENDERER as the
bisect levers. Also reserve extra_hw_frames=4 on the VAAPI decoder: the
presenter pins mapped surfaces past receive_frame, and the fixed pool recycling
a surface the renderer still samples is intermittent block corruption anywhere.

Input (Deck): with Steam Input ON for Punktfunk, SDL sees only Steam's virtual
X360 pad — the right trackpad arrives as a plain right stick and the left
trackpad/paddles/gyro not at all, silently. The client now checks once the
post-attach enumeration settles and raises a toast + warn naming the fix
(disable Steam Input for the shortcut). The host logs a one-shot warning when
InputPlumber is running (Bazzite default) since it can grab the virtual Deck
pad and re-emit it under a different identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:56:06 +00:00
enricobuehler 76ff616dcf fix(flatpak): drop --socket=pipewire (unknown to the builder) — keep the xdg-run bind
apple / swift (push) Successful in 1m13s
apple / screenshots (push) Successful in 5m46s
android / android (push) Successful in 11m8s
ci / web (push) Successful in 46s
ci / docs-site (push) Successful in 57s
ci / rust (push) Successful in 12m53s
ci / bench (push) Successful in 4m44s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 4m34s
flatpak / build-publish (push) Successful in 4m5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m54s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m45s
docker / deploy-docs (push) Successful in 18s
The v0.7.2 flatpak build failed: `error: Unknown socket type pipewire` — this
flatpak-builder toolchain (and the Deck's flatpak 1.16 override CLI) don't
accept --socket=pipewire. --filesystem=xdg-run/pipewire-0 binds the same native
socket and is the portable form already validated on-Deck (pipewire-0 appears
in the sandbox, client audio node registers, no pw-connect error). Keep only
that + --socket=pulseaudio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 09:20:52 +00:00
7 changed files with 231 additions and 17 deletions
+17
View File
@@ -116,6 +116,23 @@ pub fn run() -> glib::ExitCode {
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.init();
// Steam launches its shortcuts with SDL_GAMECONTROLLER_IGNORE_DEVICES naming every
// physical pad Steam Input has virtualized — SDL then hides the real device so games
// only see the virtual X360 pad. Right for games, wrong for us: capturing the Deck's
// built-in controller (trackpads/paddles/gyro, 28DE:1205) needs SDL's HIDAPI driver
// to enumerate the REAL device, and the built-in pad can never leave Steam Input
// ("Steam Controller" is always-required), so this filter is the only off switch we
// get. Clear it while still single-threaded (the gamepad worker starts with the UI);
// we dedupe the virtual pad ourselves (`gamepad.rs` `active_id` skips steam_virtual).
for var in [
"SDL_GAMECONTROLLER_IGNORE_DEVICES",
"SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT",
] {
if let Ok(v) = std::env::var(var) {
tracing::info!(var, value = %v, "clearing Steam's SDL device filter");
std::env::remove_var(var);
}
}
// Headless pairing path (no GTK window): `--pair <PIN> --connect host[:port] [--name N]`.
// Used by the Decky plugin (a GTK dialog can't pop under gamescope) and for scripting.
if let Some(pin) = crate::cli::arg_value("--pair") {
+102 -10
View File
@@ -551,6 +551,14 @@ struct Worker<'a> {
/// switch / detach so a contact held at that moment doesn't stick. surface 0 = the legacy single
/// touchpad, 1/2 = a Steam left/right pad.
held_touches: std::collections::HashSet<(u8, u8)>,
/// Per Steam-pad surface (index 0 = left/surface 1, 1 = right/surface 2): the last wire
/// coordinates + whether a finger is on it. Pad CLICKS arrive as buttons with no position,
/// so the click forward reuses the surface's live contact point.
surface_last: [(i16, i16, bool); 2],
/// Steam-pad clicks currently held (surface1 indexed): keeps the click bit asserted
/// through touch-motion frames (which would otherwise clear it host-side) and lets the
/// flush lift a click held across detach/pad-switch.
held_clicks: [bool; 2],
last_accel: [i16; 3],
/// Raises the UI escape signal; the escape chord fires it once per press.
escape_tx: async_channel::Sender<()>,
@@ -681,6 +689,24 @@ impl Worker<'_> {
}
*v = i32::MIN;
}
// Lift any Steam-pad click held at this moment — a click that survives a
// detach/pad-switch would leave the host's pad pressed forever.
for i in 0..2usize {
if std::mem::take(&mut self.held_clicks[i]) {
let (x, y, _) = self.surface_last[i];
let _ = c.send_rich_input(RichInput::TouchpadEx {
pad: 0,
surface: (i as u8) + 1,
finger: 0,
touch: false,
click: false,
x,
y,
pressure: 0,
});
}
}
self.surface_last = [(0, 0, false); 2];
// Lift any touchpad contact the host still believes is down (surface 0 = legacy pad).
for (surface, finger) in self.held_touches.drain() {
let rich = if surface == 0 {
@@ -709,6 +735,8 @@ impl Worker<'_> {
self.held_buttons.clear();
self.last_axis = [i32::MIN; 6];
self.held_touches.clear();
self.held_clicks = [false; 2];
self.surface_last = [(0, 0, false); 2];
}
// A held chord doesn't survive a flush (detach / pad-switch) — clear its latches too.
self.reset_chord();
@@ -789,26 +817,29 @@ impl Worker<'_> {
y: f32,
active: bool,
) {
let Some(c) = self.attached.as_ref() else {
let Some(c) = self.attached.clone() else {
return;
};
let multi = self
.open
.as_ref()
.filter(|(id, _)| *id == which)
.map(|(_, p)| p.touchpads_count() >= 2)
.unwrap_or(false);
let multi = self.is_multi_touchpad(which);
let (cx, cy) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
let surface = if multi { (touchpad as u8) + 1 } else { 0 };
let rich = if multi {
let (wx, wy) = (
(cx * 65535.0 - 32768.0) as i16,
(cy * 65535.0 - 32768.0) as i16,
);
let i = (surface - 1).min(1) as usize;
self.surface_last[i] = (wx, wy, active);
RichInput::TouchpadEx {
pad: 0,
surface,
finger,
touch: active,
click: false,
x: (cx * 65535.0 - 32768.0) as i16,
y: (cy * 65535.0 - 32768.0) as i16,
// The pad's physical click is a separate BUTTON event (see forward_click) —
// carry the held state so a motion frame can't clear a click mid-press.
click: self.held_clicks[i],
x: wx,
y: wy,
pressure: 0,
}
} else {
@@ -828,6 +859,57 @@ impl Worker<'_> {
}
}
/// The open pad has two touchpads (Steam Deck / Steam Controller) — the gate for the
/// `TouchpadEx` surface encoding and the pad-click button re-route.
fn is_multi_touchpad(&self, which: u32) -> bool {
self.open
.as_ref()
.filter(|(id, _)| *id == which)
.map(|(_, p)| p.touchpads_count() >= 2)
.unwrap_or(false)
}
/// SDL's Steam Deck mapping delivers the pad CLICKS as gamepad buttons — the generic
/// `touchpad` button is the LEFT pad's click and `misc2` the RIGHT's (SDL_gamepad_db.h
/// `touchpad:b17,misc2:b16`). They must NOT ride the button plane: it has no surface
/// identity, and the host maps `BTN_TOUCHPAD` to the RIGHT pad (DualSense convention) —
/// which is exactly "a left-pad click registers on the right pad". Only for the open
/// multi-touchpad pad; a DualSense's single `touchpad` button stays a wire button.
fn steam_click_surface(&self, which: u32, button: sdl3::gamepad::Button) -> Option<u8> {
use sdl3::gamepad::Button;
if !self.is_multi_touchpad(which) {
return None;
}
match button {
Button::Touchpad => Some(1),
Button::Misc2 => Some(2),
_ => None,
}
}
/// Forward a Steam-pad click on the rich plane, bound to its surface. Click events carry
/// no position, so reuse the surface's live contact point; a physical click implies
/// contact, so `touch` stays asserted while the click is down even if the touch event
/// hasn't arrived yet (event-order safety).
fn forward_click(&mut self, surface: u8, down: bool) {
let Some(c) = self.attached.clone() else {
return;
};
let i = (surface - 1).min(1) as usize;
self.held_clicks[i] = down;
let (x, y, touching) = self.surface_last[i];
let _ = c.send_rich_input(RichInput::TouchpadEx {
pad: 0,
surface,
finger: 0,
touch: touching || down,
click: down,
x,
y,
pressure: 0,
});
}
/// Publish the pad list, active pad, and pin to the UI-facing mutexes.
fn publish(&self) {
let mut list: Vec<PadInfo> = self
@@ -935,6 +1017,10 @@ impl Worker<'_> {
}
}
Event::ControllerButtonDown { which, button, .. } if active == Some(which) => {
if let Some(surface) = self.steam_click_surface(which, button) {
self.forward_click(surface, true);
return;
}
let Some(c) = self.attached.clone() else {
return;
};
@@ -945,6 +1031,10 @@ impl Worker<'_> {
}
}
Event::ControllerButtonUp { which, button, .. } if active == Some(which) => {
if let Some(surface) = self.steam_click_surface(which, button) {
self.forward_click(surface, false);
return;
}
let Some(c) = self.attached.clone() else {
return;
};
@@ -1158,6 +1248,8 @@ fn run(
last_axis: [i32::MIN; 6],
held_buttons: Vec::new(),
held_touches: std::collections::HashSet::new(),
surface_last: [(0, 0, false); 2],
held_clicks: [false; 2],
last_accel: [0; 3],
escape_tx: escape_tx.clone(),
disconnect_tx: disconnect_tx.clone(),
+33
View File
@@ -280,6 +280,39 @@ impl SessionUi {
if self.app.fullscreen || self.app.settings.borrow().fullscreen_on_stream {
self.app.window.fullscreen();
}
// A Deck streaming without its raw built-in controller is invisible degradation:
// SDL sees only Steam's virtual X360 pad, so the right trackpad arrives at the
// host as whatever Steam's template synthesizes (a right stick by default) and
// the left trackpad, paddles and gyro not at all. The built-in pad can never
// leave Steam Input ("Steam Controller" is always-required in the shortcut's
// matrix — Disable Steam Input only affects other brands), so raw capture rides
// the session-scoped Valve HIDAPI drivers + the cleared SDL device filter (see
// `app::run`). The real 28DE:1205 identity enumerates shortly after attach —
// check once that settles and say so, instead of streaming silently degraded.
if crate::gamepad::is_steam_deck() {
let app = self.app.clone();
let stop = self.stop.clone();
glib::timeout_add_seconds_local_once(4, move || {
if stop.load(std::sync::atomic::Ordering::Relaxed) {
return; // session already over
}
if app.gamepad.active().is_none_or(|pad| pad.steam_virtual) {
tracing::warn!(
"the Deck's raw built-in controller (28DE:1205) never enumerated \
— only Steam's virtual pad is visible, so trackpads, paddles and \
gyro can't be captured (sticks + buttons still work). Check the \
startup log for SDL_GAMECONTROLLER_IGNORE_DEVICES and the \
Settings controller list."
);
let toast = adw::Toast::new(
"Steam is only exposing its virtual gamepad — trackpads, paddles \
and gyro won't reach the game (sticks and buttons still work).",
);
toast.set_timeout(12);
app.toasts.add_toast(toast);
}
});
}
self.page = Some(p);
}
+27
View File
@@ -187,6 +187,25 @@ impl Decoder {
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| pref.to_string());
// The Steam Deck's VAAPI zero-copy path renders corrupt/gray/washed-out — validated live;
// software decode is clean, correct-colour, and the Deck's APU handles 1280×800 HEVC
// easily. Likely cause: since Mesa 25.1 radeonsi exports VCN decode surfaces TILED (with
// AMD modifiers) instead of linear, and inside the Flatpak both the VAAPI driver and GTK's
// GL come from the runtime's Mesa 26.x — GTK's tiled-NV12 dmabuf import mishandles the new
// layout (desktop AMD/Intel boxes validated Tier-1 ran distro Mesa with linear export).
// So `auto` resolves to software on a Deck; an explicit `vaapi` (Settings or
// PUNKTFUNK_DECODER=vaapi) still forces the hw path for testing — the first-frame
// descriptor dump logs the modifier (LINEAR = 0x0), and GSK_RENDERER=ngl|vulkan bisects
// the import side.
let choice = if (choice == "auto" || choice.is_empty()) && crate::gamepad::is_steam_deck() {
tracing::info!(
"Steam Deck — defaulting to software decode (AMD VAAPI dmabuf is broken on this \
SteamOS+Mesa combo); set the decoder to `vaapi` to override"
);
"software".to_string()
} else {
choice
};
if choice != "software" {
match VaapiDecoder::new(codec_id) {
Ok(v) => {
@@ -456,6 +475,14 @@ impl VaapiDecoder {
(*ctx).get_format = Some(pick_vaapi);
(*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32;
(*ctx).thread_count = 1; // hwaccel: threads only add latency
// The presenter holds mapped surfaces PAST receive_frame (the paintable's
// current texture + the newest frame in flight each pin one until GDK's
// release func) — surfaces libavcodec doesn't know are missing from its
// fixed-size VAAPI pool. Without headroom the decoder can recycle a surface
// the renderer is still sampling (intermittent block corruption) or fail
// allocation under scheduling jitter.
(*ctx).extra_hw_frames = 4;
let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut());
if r < 0 {
let mut ctx = ctx;
@@ -276,12 +276,43 @@ impl DeckTransport {
}
}
/// One-shot diagnostic: InputPlumber (shipped and enabled by default on Bazzite) hidraw-grabs
/// controllers it decides to manage and re-emits them under a different identity — historically
/// the Deck config re-emitted an Xbox Elite pad with the trackpads routed to a mouse target. If
/// it grabs our virtual Deck, everything downstream of hid-steam looks wrong (trackpads surface
/// as a stick/mouse, gyro vanishes) while punktfunk's own logs stay clean — so name the suspect
/// up front. Best-effort process-name scan; no dependency on its D-Bus API.
fn warn_if_inputplumber() {
use std::sync::atomic::{AtomicBool, Ordering};
static ONCE: AtomicBool = AtomicBool::new(true);
if !ONCE.swap(false, Ordering::Relaxed) {
return;
}
let running = std::fs::read_dir("/proc")
.ok()
.into_iter()
.flatten()
.flatten()
.any(|e| {
std::fs::read_to_string(e.path().join("comm")).is_ok_and(|c| c.trim() == "inputplumber")
});
if running {
tracing::warn!(
"InputPlumber is running on this host — if it manages the virtual Steam Deck pad, \
games see InputPlumber's re-emitted device instead (trackpads may arrive as a \
stick/mouse, gyro may vanish). Check `inputplumber devices` and exclude the \
virtual pad from management if inputs look remapped."
);
}
}
/// Open the best Steam-Input-promotable Deck transport available, in preference order:
/// **`raw_gadget` (SteamOS validated fast-path) → `usbip`/`vhci_hcd` (universal, Secure-Boot-clean)
/// → UHID (universal, but `Interface: -1` so Steam Input won't promote it).** Each rung degrades to
/// the next on failure, so a host lacking the gadget modules still gets a *promotable* Deck via
/// usbip, and one lacking both still gets a working (if non-promoted) UHID pad.
fn open_transport(idx: u8) -> Result<DeckTransport> {
warn_if_inputplumber();
use crate::inject::{steam_gadget, steam_usbip};
// 1. raw_gadget — the validated SteamOS fast-path (default on there).
if steam_gadget::gadget_preferred() {
+16 -1
View File
@@ -1911,6 +1911,13 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
/// two Decks — confirmed conflict-prone on a Deck-as-host (the physical `28DE:1205` + Steam's
/// `28DE:11FF` XInput output pad are both live). HID device dirs are named `BUS:VID:PID.INST`
/// (uppercase); a UHID virtual device resolves through `/devices/virtual/…`, a real one does not.
///
/// Punktfunk's OWN virtual Decks must never count: the usbip/gadget transports present a real USB
/// device (vhci resolves through `vhci_hcd`, NOT `/devices/virtual/`), so a just-ended session's
/// pad still detaching — or a concurrent session's live one — read as "physical" and degraded
/// every back-to-back Deck session to DualSense (observed live on Bazzite 2026-07-04). Ours are
/// recognizable by the `PFDK…` serial ([`steam_proto::deck_serial`]) in `HID_UNIQ`, with the
/// vhci path as belt and braces.
#[cfg(target_os = "linux")]
fn physical_steam_controller_present() -> bool {
let Ok(entries) = std::fs::read_dir("/sys/bus/hid/devices") else {
@@ -1920,8 +1927,16 @@ fn physical_steam_controller_present() -> bool {
if !e.file_name().to_string_lossy().contains(":28DE:") {
return false;
}
if std::fs::read_to_string(e.path().join("uevent"))
.is_ok_and(|u| u.lines().any(|l| l.starts_with("HID_UNIQ=PFDK")))
{
return false; // one of our own virtual Decks
}
match std::fs::read_link(e.path()) {
Ok(target) => !target.to_string_lossy().contains("/virtual/"),
Ok(target) => {
let t = target.to_string_lossy();
!t.contains("/virtual/") && !t.contains("vhci_hcd")
}
Err(_) => true,
}
})
+5 -6
View File
@@ -68,12 +68,11 @@ finish-args:
# PulseAudio shim — so it needs the real `pipewire-0` socket in the sandbox. With only
# --socket=pulseaudio the sandbox has just `pulse/native`, no `pipewire-0`, and playback +
# mic both die with "pw connect (is PipeWire running in this session?)" (observed live on the
# Deck in Gaming Mode). --socket=pipewire is the canonical grant; --filesystem=xdg-run/
# pipewire-0 binds the same socket portably (validated on-Deck: it makes pipewire-0 appear in
# the sandbox where --socket=pipewire's CLI validation was flaky). Neither needs the
# camera/portal dance (that's only for camera nodes). --socket=pulseaudio stays as a fallback
# for any pulse-only path. ---
- --socket=pipewire
# Deck in Gaming Mode). We bind the native socket via --filesystem=xdg-run/pipewire-0 (NOT
# --socket=pipewire: this flatpak-builder toolchain rejects it as an "Unknown socket type",
# and the Deck's flatpak 1.16 override CLI does too — the filesystem bind is the portable
# form, validated on-Deck to make pipewire-0 appear + the client register its audio node).
# --socket=pulseaudio stays as a fallback for any pulse-only path. ---
- --filesystem=xdg-run/pipewire-0
- --socket=pulseaudio
# --- network: QUIC control + UDP data plane + mDNS discovery (_punktfunk._udp) ---