feat(client/pads): stop forwarding the pad while the Steam overlay owns it

On a Deck in Gaming Mode the Steam menu and the QAM are driven by the SAME
physical controller the client forwards, so opening either one moved the game
on the host as well as Steam's UI — a second, invisible player. Steam Input
masks a normal game here; it cannot mask us, because masking happens on Steam
Input's virtual pad and we deliberately forward the REAL one (28DE:1205 — the
virtual pad has no gyro, trackpads or paddles).

SDL ships the exact behaviour we want and it is on by default: presses are
dropped while the process has windows but no keyboard focus, releases still get
through. It CANNOT fire on a Deck. gamescope resolves focus per Xwayland ctx
and the client sits alone in its own, so the Steam overlay — which lives in the
root ctx — never takes our X focus away and no FocusOut is ever generated.
Measured on glass: with the QAM open, X input focus inside the client's ctx
stayed on its window for the whole 4 s, while GAMESCOPE_FOCUSED_APP flipped to
769 (Steam) and GAMESCOPE_FOCUSED_APP_GFX stayed on the app.

So the signal is explicit. `overlay_focus` watches those two atoms on the
gamescope root ctx — which is NOT our own $DISPLAY under `--xwayland-count 2`,
hence the socket-directory walk and the flatpak filesystem line — and the
presenter ORs it with window focus into one `set_masked`.

Masking is deliberately not `set_forwarding`: that closes the slot and sends
GamepadRemove, so the game would see a controller UNPLUG every time somebody
opened the QAM. This keeps every slot open and only stops the transitions,
after flushing what the host believes is held so a stick deflected at
overlay-open stops steering instead of freezing at its last value. On the way
back, held buttons are adopted rather than replayed — the A that picked a QAM
row must not fire in the game as it closes — while axes are re-sent, since a
stick has no press to ghost and SDL only speaks on change.

Fails open throughout: no gamescope, no X, or an unreadable signal all leave
forwarding exactly as it was. `PUNKTFUNK_OVERLAY_MASK=0` opts out.
This commit is contained in:
2026-08-09 00:55:16 +02:00
parent 54666e66da
commit 5cbaca7789
7 changed files with 500 additions and 1 deletions
+8
View File
@@ -65,6 +65,14 @@ finish-args:
- --socket=wayland # GTK4 native Wayland window (the client is Wayland-first)
- --socket=fallback-x11 # Xwayland fallback when no Wayland socket is exposed
- --share=ipc # required alongside X11 for shared-memory surfaces
# Gaming Mode's overlay signal lives on a DIFFERENT X server than ours. gamescope runs
# `--xwayland-count 2`: Steam and the GAMESCOPE_FOCUSED_APP/_GFX atoms are on the first,
# the app is handed the second, so `$DISPLAY` alone can never see them — and --socket=x11
# would not help, since flatpak binds only the ONE socket named by DISPLAY. Read-only
# access to the socket directory is what lets `overlay_focus` reach the root ctx and stop
# forwarding the pad while the Steam menu / QAM is up. gamescope's Xwayland takes
# unauthenticated local connections, so no cookie has to cross with it.
- --filesystem=/tmp/.X11-unix:ro
# --- GPU + all input devices ---
# --device=all (not just --device=dri): covers the GPU render node (VAAPI HEVC decode + GL),
# evdev joysticks, AND the hidraw CHAR devices SDL3's HIDAPI needs for DualSense touchpad/