From 9310d198570e756cfccc9f09eece00e5fa7871e1 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:28:11 -0400 Subject: [PATCH] fix: backport upstream gamescope fix for games left behind Steam's loading screen (#401) --- docs/development/steam-focus-debugging.md | 89 +++++++++++++++++++ tools/gamescope/PATCHES.md | 6 ++ ...ake-override-redirect-from-mapnotify.patch | 35 ++++++++ tools/gamescope/release.env | 4 +- 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 docs/development/steam-focus-debugging.md create mode 100644 tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch diff --git a/docs/development/steam-focus-debugging.md b/docs/development/steam-focus-debugging.md new file mode 100644 index 000000000..f18062edc --- /dev/null +++ b/docs/development/steam-focus-debugging.md @@ -0,0 +1,89 @@ +# 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/-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 `Download/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 ` 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. diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index ad4fc4b34..acb345515 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -38,6 +38,12 @@ library list, before anything is published. iconify request is remembered and the window goes back to NormalState before input returns to it, then focus is handed over again. `GAMESCOPE_RESTORE_FOCUS_WINDOW` on the root window asks for the same restore from outside (the session script's resume watcher). +- `0114-take-override-redirect-from-mapnotify.patch` - upstream (ValveSoftware/gamescope 3829340), + verbatim; drop it once the runtime's gamescope includes it. Wine creates a game's window + override-redirect and makes it managed before mapping it, which sends no X event. When gamescope + read the window's attributes first, it kept the window as override-redirect and left it out of + `GAMESCOPE_FOCUSABLE_WINDOWS`, so the client kept its loading screen over the running game + (Skyrim SE, A Plague Tale: Innocence, on the games' own Xwayland). MapNotify now refreshes the flag. Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. diff --git a/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch new file mode 100644 index 000000000..f2ea4a16f --- /dev/null +++ b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch @@ -0,0 +1,35 @@ +From: Matthew Schwartz +Subject: [PATCH] steamcompmgr: take override-redirect from MapNotify + +Wine creates a toplevel as override-redirect and flips it to managed with +XChangeWindowAttributes while it is unmapped, which generates no X event. +When add_win reads the attributes before the flip, the window stays cached +as override-redirect and is left out of GAMESCOPE_FOCUSABLE_APPS. Steam +then keeps its loading screen over the game, as with A Plague Tale: +Innocence (752590). Before 2238420d, the focus nudge's move usually +refreshed the flag. + +MapNotify carries the current override-redirect state, so refresh the +cached flag there rather than re-reading the attributes. + +Closes: #2457 +--- + src/steamcompmgr.cpp | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index f00b62c..cb3b9c6 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -8778,7 +8778,11 @@ void xwayland_ctx_t::Dispatch() + steamcompmgr_win_t * w = find_win(ctx, ev.xmap.window); + + if (w && w->xwayland().id == ev.xmap.window) ++ { ++ // Wine flips override-redirect while unmapped and the flip itself sends no event. ++ w->xwayland().a.override_redirect = ev.xmap.override_redirect; + map_win(ctx, ev.xmap.window, ev.xmap.serial); ++ } + break; + } + case UnmapNotify: diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index a0c6c614f..9f895d08d 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p8 -GAMESCOPE_SHA256=65fcc17af1aee4944a42ce1e688db61ceb10db5f299f0de75e338f56646e2953 +GAMESCOPE_TAG=gamescope-3.16.29-p9 +GAMESCOPE_SHA256=7123a37bdbcd0d76d4bd1d7490e5d2017221e036395fdd48c1e92458594ca769