mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
90 lines
5.5 KiB
Markdown
90 lines
5.5 KiB
Markdown
# 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:
|
|
|
|
1. **gamescope lists candidates.** For each server it collects windows that are mapped, of class
|
|
InputOutput, not fully transparent (`_NET_WM_WINDOW_OPACITY` above 0), not an overlay, and
|
|
carry an app id (`STEAM_GAME`). It publishes them on `:0`'s root as `GAMESCOPE_FOCUSABLE_APPS`
|
|
and `GAMESCOPE_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.
|
|
2. **Steam picks an order.** The client reads those lists and writes
|
|
`GAMESCOPECTRL_BASELAYER_APPID`, for example `413091, 489830, 769`. When the running game has
|
|
no window in the focusable list, Steam puts itself (769) ahead of it.
|
|
3. **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_APP` stays `769`; `GAMESCOPECTRL_BASELAYER_APPID` has `769` ahead of the
|
|
game's app id; the game's window is missing from `GAMESCOPE_FOCUSABLE_WINDOWS`.
|
|
- On `:1` the game window is mapped, `WM_STATE` Normal, `STEAM_GAME` set, with no opacity,
|
|
`_NET_WM_STATE` or overlay properties.
|
|
- gamescope's `focus_info` shows 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:
|
|
|
|
```sh
|
|
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 under `linuxfs/tmp/.X11-unix` can be stale. Nothing answers
|
|
while the session is in the background or the screen is off (the session is stopped).
|
|
- The `VAR=… cmd` prefix does not survive `timeout` or `nohup`; put the command in a small
|
|
`/system/bin/sh` script in `/data/local/tmp` and run that.
|
|
- gamescope's focus state, written to the session's `session.log` (in
|
|
`Download/DroidDeck/<date>-NN-steam/`):
|
|
|
|
```sh
|
|
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_info
|
|
```
|
|
|
|
It 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 add
|
|
`PATH=/opt/gstest:/usr/local/bin:/usr/bin:/bin` to `Android/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/bin` at every session start.
|
|
- Never `pkill -f <pattern>` inside `su -c '…'`: the pattern matches that shell's own command line.
|
|
Killing `SkyrimSE.exe` that way took the whole session down. Kill by pid.
|
|
- Skyrim's menus take adb key events (`input keyevent 20` down, `66` enter); taps do nothing.
|