Skip to content

fix(overlay): stop gamescope detection matching its own command line#212

Merged
srsholmes merged 2 commits into
mainfrom
fix/overlay-gamescope-detect-self-match
Jul 15, 2026
Merged

fix(overlay): stop gamescope detection matching its own command line#212
srsholmes merged 2 commits into
mainfrom
fix/overlay-gamescope-detect-self-match

Conversation

@srsholmes

Copy link
Copy Markdown
Owner

Problem

The overlay reports Gaming Mode on every desktop session. Both a user's CachyOS desktop and mine log this with no gamescope running at all:

[loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0

Confirmed live on my machine — pgrep -x gamescope-wl returns nothing, yet the unit exports GAMESCOPE_DISPLAY=:0 and the app follows it: [display-detect] using $GAMESCOPE_DISPLAY=:0 on a plain Plasma Wayland session.

Root cause

if [ -z "$GS_DISPLAY" ] && pgrep -f "gamescope[- ]" > /dev/null 2>&1; then
  GS_DISPLAY=":0"
  [ -z "$GS_WAYLAND" ] && GS_WAYLAND="gamescope-0"
fi

pgrep -f matches against full command lines. This entire script is the argv of the sh -c that systemd runs, and the branch body contains GS_WAYLAND="gamescope-0" — which matches the pattern gamescope[- ]. pgrep excludes itself but not its parent shell, so the condition is unconditionally true.

Verified by running the exact ExecStart payload on a box with no gamescope process: it still printed GAMESCOPE_DISPLAY=:0, and pgrep reported its own shell PIDs as the matches.

(systemd strips the # comments before exec, so the trigger is the live gamescope-0 assignment, not the prose around it — confirmed against the ExecStart property dump.)

Fix

Match the real comm with pgrep -x:

if [ -z "$GS_DISPLAY" ] && { pgrep -x gamescope-wl > /dev/null 2>&1 || pgrep -x gamescope > /dev/null 2>&1; }; then

gamescope's kernel comm is gamescope-wl — which is exactly why the original pgrep -x gamescope failed and prompted the switch to -f. Verified against a real process:

  PID   COMM              ARGS
  15767 gamescope-wl      gamescope -W 320 -H 240 -- sleep 25

pgrep -x gamescope-wl → match; pgrep -x gamescope → no match. Both names are checked in case a build differs. -x matches comm (sh here), so it cannot self-match.

Verification

Both directions tested on CachyOS by driving the real ExecStart from this file (probe unit generated from the service via sed, echoing instead of exec'ing the launcher):

Case Result
Desktop, no gamescope GAMESCOPE_DISPLAY=unset GAMESCOPE_WAYLAND_DISPLAY=unset ✅ (was :0 / gamescope-0)
Real gamescope running (pid 16322) GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0 ✅ still detected

So the false positive is gone with no regression to genuine gamescope detection.

Scope

scripts/dev-overlay.sh:49 keeps pgrep -f deliberately — it lives in a script file, so the pattern is not in that process's argv and cannot self-match. Left alone rather than churned.

Applied to both loadout-overlay.service and the scripts/install.sh heredoc, kept byte-identical (per c0f36c4, these drift).

Why this matters beyond the log line

Gaming Mode isn't cosmetic — it gates Steam SIGSTOP-on-open, gamescope-resolution window sizing, and CEF plugin injection. A desktop session falsely in Gaming Mode takes all those paths.

Notes

Separate from #211 (the mesa/zink crash fix) so each can be reverted independently. Both touch the same ExecStart region, so whichever lands second needs a rebase. #211 is the one that actually gets the overlay open again; this one is a correctness fix found alongside it.

🤖 Generated with Claude Code

The overlay reported Gaming Mode on every desktop session. Both a user's
CachyOS desktop and mine logged this with no gamescope running at all:

  [loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0

`pgrep -f` matches against full command lines. This whole script is the
argv of the `sh -c` that systemd runs, and the branch body contains
GS_WAYLAND="gamescope-0" -- so the pattern "gamescope[- ]" matched our
own shell. pgrep excludes itself but not its parent, which made the
condition unconditionally true. (systemd strips the # comments, so the
trigger is the live assignment, not the prose around it.)

Match the real comm with `pgrep -x` instead. gamescope's kernel comm is
"gamescope-wl", which is why the original `pgrep -x gamescope` failed and
prompted the switch to -f in the first place; check both names. `-x`
matches comm -- "sh" here -- so it cannot self-match.

Verified both directions on CachyOS, driving the real ExecStart:

  desktop, no gamescope  -> GAMESCOPE_DISPLAY=unset          (was :0)
  real gamescope running -> GAMESCOPE_DISPLAY=:0, gamescope-0 (still detected)

scripts/dev-overlay.sh keeps `pgrep -f`: it lives in a script file, so the
pattern is not in that process's argv and cannot self-match.

Applied to both the canonical unit and the installer heredoc, which are
kept byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pgrep branch is not a fallback on SteamOS -- it is the only thing
that resolves the display there. Steam's environment in Gaming Mode
carries GAMESCOPE_WAYLAND_DISPLAY and DISPLAY but *not*
GAMESCOPE_DISPLAY, so the sed for it returns empty and control always
reaches the pgrep test. That made Gaming Mode detection rest entirely on
gamescope's comm string: if it is ever renamed, the Deck silently
degrades to desktop mode with no error, and the ":0" it assumes is a
guess that happens to be right today.

Steam already knows the inner X display, so read it. The pgrep -x test
stays as a genuine last resort.

Verified on SteamOS Gaming Mode (gamescope-wl live, Radeon 8060S):

  steam environ:  GAMESCOPE_WAYLAND_DISPLAY=gamescope-0
                  DISPLAY=:0
                  (no GAMESCOPE_DISPLAY)

  real ExecStart: DISPLAY=:0 GAMESCOPE_DISPLAY=:0
                  GAMESCOPE_WAYLAND_DISPLAY=gamescope-0   (unchanged)

  pgrep branch forced false:
                  GS_DISPLAY=:0 GS_WAYLAND=gamescope-0    (still resolves)

so the display is now derived rather than guessed, with no change to the
result on a real device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@srsholmes

Copy link
Copy Markdown
Owner Author

Verified on real SteamOS Gaming Mode — no regression, root cause confirmed, one hardening pushed

Tested on SteamOS in live Gaming Mode (gamescope-wl running, Radeon 8060S, systemd 257).

Root cause confirmed from the live argv

The ExecStart property dump on the installed unit proves it directly — systemd strips the # comments, and what's left in the shell's own argv contains both the pattern and a string matching it:

argv[]=/bin/sh -c … if [ -z "$GS_DISPLAY" ] && pgrep -f "gamescope[- ]" > /dev/null 2>&1; then
                      GS_DISPLAY=":0";  [ -z "$GS_WAYLAND" ] && GS_WAYLAND="gamescope-0"; fi; …

So pgrep -f "gamescope[- ]" matched its own parent shell. The PR's analysis is exactly right, including the note that systemd strips the comments before exec (confirmed here on systemd 257 — the prose is absent from the argv, the gamescope-0 assignment is not).

No regression in Gaming Mode

Drove both logics against the real thing:

Result
Old (pgrep -f) DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0
New (pgrep -x) DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0

gamescope-wl is the real comm here (pgrep -x gamescope-wl → match; pgrep -x gamescope → no match), so detection is preserved. Ship it.

It also fixes a live bug on Deck desktop mode, where the self-match currently forces gamescopeMode=true (detectGamescopeMode() keys off GAMESCOPE_DISPLAY) on a session with no gamescope at all.

⚠️ The one thing the description gets wrong — that branch isn't a fallback on SteamOS

Steam's environ in Gaming Mode on this device:

GAMESCOPE_WAYLAND_DISPLAY=gamescope-0
DISPLAY=:0
                          ← no GAMESCOPE_DISPLAY

There is no GAMESCOPE_DISPLAY, so step 1's sed returns empty and control always reaches the pgrep test. That branch is the only thing resolving the display on SteamOS — load-bearing, not a fallback.

That's fine today, but it means Gaming Mode detection now rests entirely on a comm string plus a hardcoded :0. If gamescope ever renames its comm, the Deck silently degrades to desktop mode with no error.

Hardening pushed (790260a)

Steam already knows the inner X display, so read it instead of guessing:

    # SteamOS exports GAMESCOPE_WAYLAND_DISPLAY into steam's environment but
    # not GAMESCOPE_DISPLAY, so the sed above comes back empty in Gaming Mode
    # and the pgrep branch below is what actually resolves the display there.
    # Steam already knows the inner X display, so read it rather than letting
    # the fallback assume ":0".
    if [ -z "$GS_DISPLAY" ] && [ -n "$GS_WAYLAND" ]; then \
      GS_DISPLAY=$(tr "\\0" "\\n" < "/proc/$PID/environ" | sed -n "s/^DISPLAY=//p" | head -n1); \
    fi; \

pgrep -x stays as a genuine last resort.

Verified — with the pgrep branch forced false, the steam-environ path alone still resolves it:

steam-env only: GS_DISPLAY=:0 GS_WAYLAND=gamescope-0

and the full real ExecStart is unchanged on a live device:

[loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0

Same answer, now derived rather than assumed, and no longer dependent on comm matching.

🤖 Reviewed and tested with Claude Code on SteamOS Gaming Mode

@srsholmes

Copy link
Copy Markdown
Owner Author

✅ Verified end-to-end on a real Steam Deck — both modes, installed build

Installed this branch's unit on SteamOS and drove both session modes. All four readings measured on hardware:

Gaming Mode Desktop Mode
main (pgrep -f) GAMESCOPE_DISPLAY=:0 GAMESCOPE_DISPLAY=:0wrong — no gamescope running
this PR (pgrep -x) GAMESCOPE_DISPLAY=:0 GAMESCOPE_DISPLAY=unset

The bug reproduces exactly as described, and the fix corrects it with no change to Gaming Mode.

Gaming Mode — no regression

Installed build, fresh PID, byte-identical to the pre-fix baseline:

old build (PID 3209):   [loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0
#212 build (PID 33101): [loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=:0 GAMESCOPE_WAYLAND_DISPLAY=gamescope-0

NRestarts=0, active (running). Live ExecStart argv confirmed to carry the new logic — pgrep -f is stripped along with its explanatory comment (systemd 257), pgrep -x gamescope-wl and the steam-DISPLAY derivation are both live.

Desktop Mode — the fix, and the behavioural check

XDG_SESSION_TYPE=wayland   DISPLAY=:0
gamescope-wl: not running
gamescope:    not running

Jul 15 10:04:09  [loadout-overlay] DISPLAY=:0 GAMESCOPE_DISPLAY=unset GAMESCOPE_WAYLAND_DISPLAY=unset

And the overlay hides and shows correctly in Desktop Mode — confirmed by the maintainer on the installed build.

That last part was the only real risk in this PR and worth stating plainly: this flips gamescopeMode from true to false for the first time on a Deck desktop, which changes window sizing and the atoms path (detectGamescopeScreenSizeSync() vs the desktop path). The log line only proves detection; it doesn't prove nothing downstream was quietly relying on the false positive. It isn't — the overlay opens and closes normally.

Note on the pgrep branch — it's load-bearing on SteamOS

Worth recording, since the description frames it as a fallback. Steam's environ in Gaming Mode on this device:

GAMESCOPE_WAYLAND_DISPLAY=gamescope-0
DISPLAY=:0
                          ← no GAMESCOPE_DISPLAY

There is no GAMESCOPE_DISPLAY, so step 1's sed returns empty and control always reaches the pgrep test — it is the only thing resolving the display on SteamOS, not a fallback. That's why the added steam-DISPLAY derivation (790260a) matters: it takes the display from steam's own environment, so detection no longer depends on gamescope's comm string. With the pgrep branch forced false, it still resolves GS_DISPLAY=:0. pgrep -x is now a genuine last resort.

Incidental

SteamOS desktop is XDG_SESSION_TYPE=wayland with XWayland on :0, and the overlay runs there fine (NRestarts=0) — so "Wayland session has no X display" is not a failure mode on SteamOS. #214 has the real root cause of the GL crash.

Verdict: no regression found. Good to merge.

🤖 Tested with Claude Code on SteamOS — Gaming Mode + Desktop Mode, installed build

@srsholmes srsholmes merged commit a98c019 into main Jul 15, 2026
2 checks passed
@srsholmes srsholmes deleted the fix/overlay-gamescope-detect-self-match branch July 15, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant