5.5 KiB
Steam session focus: how it works and how to debug it
Written 2026-10-07 after the "game opens behind Steam" reports (Skyrim SE, A Plague Tale: Innocence, Burnout Paradise Remastered), fixed by gamescope patch 0114 (#401). It records what a game needs to get focus in a Steam session and how to inspect a live session on a device, for the next report that looks like this.
How a game gets focus
The Steam session runs two Xwayland servers, as SteamOS does: the client on :0, games on :1
(--xwayland-count 2, STEAM_MULTIPLE_XWAYLANDS=1 in droiddeck-session). Focus is decided in
three steps, and a window can drop out at each one:
- gamescope lists candidates. For each server it collects windows that are mapped, of class
InputOutput, not fully transparent (
_NET_WM_WINDOW_OPACITYabove 0), not an overlay, and carry an app id (STEAM_GAME). It publishes them on:0's root asGAMESCOPE_FOCUSABLE_APPSandGAMESCOPE_FOCUSABLE_WINDOWS(window, app id, pid triples). It leaves out of that published list any window it believes is override-redirect, 1x1, or skip-taskbar-and-pager without fullscreen. - Steam picks an order. The client reads those lists and writes
GAMESCOPECTRL_BASELAYER_APPID, for example413091, 489830, 769. When the running game has no window in the focusable list, Steam puts itself (769) ahead of it. - gamescope follows Steam. The virtual connector strategy in a Steam session is SteamControlled: the first app id in the base layer with a candidate window wins.
gamescope works from its own cached copy of each window's attributes, not from X. A window that X shows mapped and tagged can still be missing in step 1 if that cache is stale.
What the bug looked like
- On screen: Steam's black loading screen with only ABORT GAME, the game audible underneath. Resume from the Steam menu does not help.
GAMESCOPE_FOCUSED_APPstays769;GAMESCOPECTRL_BASELAYER_APPIDhas769ahead of the game's app id; the game's window is missing fromGAMESCOPE_FOCUSABLE_WINDOWS.- On
:1the game window is mapped,WM_STATENormal,STEAM_GAMEset, with no opacity,_NET_WM_STATEor overlay properties. - gamescope's
focus_infoshows server 1's own focus on the game while the global focus is Steam.
Cause: wine creates the game's window override-redirect and makes it managed before mapping it,
which sends no X event. gamescope read the attributes when the window was created and refreshed
override_redirect only from ConfigureNotify, so when its read came before wine's change it kept
the window as override-redirect and left it out of the list Steam reads. It is a race: Skyrim
failed about once in six launches on the Thor, on the first launch after a session start. Upstream
fixed it by taking the flag from MapNotify (ValveSoftware/gamescope 3829340, our patch 0114).
The launcher and the game both had window id 0x1c00003 on :1: with few clients on that server
the X server hands the game the launcher's freed id. That was not the cause (the game window came
7 s after the launcher's was destroyed), but it makes logs easy to misread.
Inspecting a live session from adb
Everything below runs on the device as the app's uid (su 10195 on the Thor, u0_a195), using
the rootfs's own binaries through its loader, so nothing has to enter proot:
R=/data/user/0/com.droiddeck.launcher/files/linuxfs
$R/usr/lib/ld-linux-aarch64.so.1 --library-path $R/usr/lib $R/usr/bin/xprop -display :0 -root \
GAMESCOPE_FOCUSED_APP GAMESCOPE_FOCUSABLE_WINDOWS GAMESCOPECTRL_BASELAYER_APPID
-
The X servers answer on their abstract sockets (
\0/tmp/.X11-unix/X0,X1); xprop finds them by itself. The socket files underlinuxfs/tmp/.X11-unixcan be stale. Nothing answers while the session is in the background or the screen is off (the session is stopped). -
The
VAR=… cmdprefix does not survivetimeoutornohup; put the command in a small/system/bin/shscript in/data/local/tmpand run that. -
gamescope's focus state, written to the session's
session.log(inDownload/DroidDeck/<date>-NN-steam/):export XDG_RUNTIME_DIR=/data/user/0/com.droiddeck.launcher/files/.wayland-rt WAYLAND_DISPLAY=gamescope-0 G="$R/usr/lib/ld-linux-aarch64.so.1 --library-path $R/usr/lib $R/usr/bin/gamescopectl" $G log_focus debug; $G focus_infoIt prints the global focus, input, keyboard and override windows, the connector strategy, and each server's own focus and keyboard focus. Disagreement between a server's focus and the global one points at step 1 or 2 above.
-
To tell a stale gamescope cache from a real X problem, make gamescope re-read the window: move it by 1 px and back (ConfigureNotify), or unmap and map it (MapNotify). If the game comes forward, gamescope's cache was wrong. A restack of a window already on top sends no event and proves nothing.
-
To test gamescope with a candidate binary without rebuilding the apk, put it in the rootfs (for example
/opt/gstest/gamescope, owned by the app's uid) and addPATH=/opt/gstest:/usr/local/bin:/usr/bin:/bintoAndroid/data/com.droiddeck.launcher/files/droiddeck-env. The session log's first lines name the gamescope that ran. Remove the line afterwards: the app stages its own gamescope over/usr/local/binat every session start. -
Never
pkill -f <pattern>insidesu -c '…': the pattern matches that shell's own command line. KillingSkyrimSE.exethat way took the whole session down. Kill by pid. -
Skyrim's menus take adb key events (
input keyevent 20down,66enter); taps do nothing.