Game Mode: Steam Deck / gamescope boots straight into the game, fullscreen

Detected from a gamescope session (XDG_CURRENT_DESKTOP, GAMESCOPE_WAYLAND_DISPLAY)
or Steam's gamepad UI (SteamGamepadUI=1); --game-mode / --no-game-mode and
PSX_GAME_MODE override. When on and a disc is set up, the launcher is
skipped and the game opens borderless fullscreen for that run (the saved
Fullscreen choice is untouched); the first run still shows the launcher so
the disc can be picked. Desktop sessions, --headless and --hidden-window are
unchanged. game_mode_test covers detection and precedence; docs/GAME_MODE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 16:07:41 -07:00
parent 7b3b1ea286
commit 477bb188b4
7 changed files with 160 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Game Mode (Steam Deck, gamescope)
On a Steam Deck in Game Mode, or any gamescope session, there is no desktop.
The launcher is an extra screen built for a mouse, and a windowed game sits
inside a fullscreen compositor anyway. Game Mode therefore boots straight into
the game, borderless fullscreen.
| Source | Effect |
| --- | --- |
| `--game-mode` / `--no-game-mode` | on / off (wins over everything) |
| `PSX_GAME_MODE=1` / `=0` | on / off |
| `XDG_CURRENT_DESKTOP` contains `gamescope`, or `GAMESCOPE_WAYLAND_DISPLAY` is set | on (detected) |
| `SteamGamepadUI=1` (started from Big Picture / Game Mode) | on (detected) |
| none of the above | off: desktop behaviour, unchanged |
When it is on:
- With a disc already set up (settings.toml `[disc] path`, or `--disc`), the
launcher is skipped and the game opens borderless fullscreen. The saved
Fullscreen choice is not changed, so Desktop Mode keeps its own.
- On the first run, with no disc yet, the launcher opens so the player can pick
their disc image. The next start goes straight in.
- `--launcher` still opens the launcher. In Game Mode, add it to the Steam
shortcut's launch options to reach the settings.
- `--headless` and `--hidden-window` runs are never affected.
The runtime logs the decision, for example
`psxrecomp: game mode (gamescope session): fullscreen, no launcher`.
+4
View File
@@ -1479,6 +1479,10 @@ if(BUILD_TESTING)
target_link_libraries(dynamic_resolution_test PRIVATE m)
endif()
add_test(NAME dynamic_resolution_test COMMAND dynamic_resolution_test)
# Game Mode (Steam Deck / gamescope) detection and precedence.
add_executable(game_mode_test tests/test_game_mode.c src/game_mode.c)
target_include_directories(game_mode_test PRIVATE include)
add_test(NAME game_mode_test COMMAND game_mode_test)
# The render-thread controller (dynrt_*) against a synthetic pipeline.
add_executable(dynamic_resolution_rt_test
tests/test_dynamic_resolution_rt.c
+27
View File
@@ -0,0 +1,27 @@
#pragma once
/* game_mode.h -- console "Game Mode" (Steam Deck Game Mode / gamescope).
*
* Under gamescope there is no desktop: the launcher window is an extra screen
* with mouse-style controls, and a windowed game sits in a fullscreen
* compositor anyway. Game Mode boots straight into the game, fullscreen, once
* the player's disc is set up (the first run still shows the launcher so the
* disc can be picked). Pure policy; the probes stay in main.cpp. */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct PsxGameModeEnv {
const char *psx_game_mode; /* PSX_GAME_MODE: "1" on, "0" off, NULL auto */
const char *xdg_current_desktop;/* "gamescope" in a gamescope session */
const char *gamescope_display; /* GAMESCOPE_WAYLAND_DISPLAY */
const char *steam_gamepad_ui; /* SteamGamepadUI=1: launched from Big Picture / Game Mode */
int cli_game_mode; /* --game-mode: 1, --no-game-mode: 0, else -1 */
} PsxGameModeEnv;
/* 1 when Game Mode applies. Precedence: the command line, then
* PSX_GAME_MODE, then detection (gamescope session or Steam's gamepad UI). */
int psx_game_mode_detect(const PsxGameModeEnv *env, const char **why);
#ifdef __cplusplus
}
#endif
+1
View File
@@ -408,6 +408,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
${PSXRECOMP_ROOT}/runtime/src/dynamic_resolution.c
${PSXRECOMP_ROOT}/runtime/src/host_time.c
${PSXRECOMP_ROOT}/runtime/src/game_mode.c
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
${PSXRECOMP_ROOT}/runtime/src/sio.c
${PSXRECOMP_ROOT}/runtime/src/memcard.c
+34
View File
@@ -0,0 +1,34 @@
/* game_mode.c -- see game_mode.h. */
#include "game_mode.h"
#include <string.h>
static int on(const char *s) { return s && s[0] && strcmp(s, "0") != 0; }
int psx_game_mode_detect(const PsxGameModeEnv *e, const char **why) {
const char *dummy;
if (!why) why = &dummy;
if (!e) { *why = "no environment"; return 0; }
if (e->cli_game_mode >= 0) {
*why = e->cli_game_mode ? "--game-mode" : "--no-game-mode";
return e->cli_game_mode;
}
if (e->psx_game_mode && e->psx_game_mode[0]) {
*why = "PSX_GAME_MODE";
return on(e->psx_game_mode);
}
if (e->xdg_current_desktop && strstr(e->xdg_current_desktop, "gamescope")) {
*why = "gamescope session";
return 1;
}
if (e->gamescope_display && e->gamescope_display[0]) {
*why = "gamescope session";
return 1;
}
if (on(e->steam_gamepad_ui)) {
*why = "Steam gamepad UI";
return 1;
}
*why = "desktop";
return 0;
}
+32
View File
@@ -107,6 +107,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "crash_trace.h"
#include "freeze_heartbeat.h"
#include "config_loader.h"
#include "game_mode.h"
#include "bios_rom_alias.h"
#include "host_path.h"
#include "launcher_device.h"
@@ -15938,6 +15939,8 @@ int main(int argc, char** argv) {
* PSX_NO_LAUNCHER env) forces it off. --launcher wins if both are given. */
bool force_launcher = false;
bool force_no_launcher = false;
/* --game-mode / --no-game-mode (game_mode.h): -1 = detect. */
int cli_game_mode = -1;
/* CLI overrides for running several instances side by side (soak fleet).
* These win over any game-config value and, crucially, work for the BIOS
* (which has no [game]-block config schema, so debug_port/renderer can't be
@@ -16031,6 +16034,10 @@ int main(int argc, char** argv) {
force_launcher = true;
} else if (std::strcmp(argv[i], "--no-launcher") == 0) {
force_no_launcher = true;
} else if (std::strcmp(argv[i], "--game-mode") == 0) {
cli_game_mode = 1;
} else if (std::strcmp(argv[i], "--no-game-mode") == 0) {
cli_game_mode = 0;
} else if (std::strcmp(argv[i], "--headless-opengl") == 0) {
g_headless = 1;
g_headless_opengl = 1;
@@ -17366,6 +17373,31 @@ int main(int argc, char** argv) {
* --no-launcher, or the persisted [launcher] skip_launcher setting — unless
* --launcher forces it back on (mirrors snesrecomp's SkipLauncher / --launcher).
* This removes the dismiss-the-launcher round-trip for scripted/debug runs. */
/* Game Mode (Steam Deck / gamescope, game_mode.h): straight into the
* game, fullscreen, once a disc is set up; the first run keeps the
* launcher so the player can pick their disc. --launcher still wins. */
{
PsxGameModeEnv gm_env{};
gm_env.psx_game_mode = std::getenv("PSX_GAME_MODE");
gm_env.xdg_current_desktop = std::getenv("XDG_CURRENT_DESKTOP");
gm_env.gamescope_display = std::getenv("GAMESCOPE_WAYLAND_DISPLAY");
gm_env.steam_gamepad_ui = std::getenv("SteamGamepadUI");
gm_env.cli_game_mode = cli_game_mode;
const char *gm_why = "";
if (psx_game_mode_detect(&gm_env, &gm_why) && !g_headless && !g_hidden_window) {
std::error_code gm_ec;
const bool disc_ready = disc_override_path ||
(!resolved_disc.empty() && std::filesystem::exists(resolved_disc, gm_ec));
if (disc_ready && !force_launcher) force_no_launcher = true;
/* Borderless (gamescope owns the display mode), this run only:
* set after the launcher would have seeded it, so a desktop
* session's saved Fullscreen choice is never rewritten. */
if (force_no_launcher) g_fullscreen = 1;
std::fprintf(stdout, "psxrecomp: game mode (%s): fullscreen, %s\n", gm_why,
force_no_launcher ? "no launcher"
: "launcher first (no disc set up yet)");
}
}
const bool want_launcher =
force_launcher ||
(!std::getenv("PSX_NO_LAUNCHER") && !force_no_launcher && !skip_launcher_setting);
+34
View File
@@ -0,0 +1,34 @@
/* game_mode: Steam Deck / gamescope detection and its precedence. */
#include "game_mode.h"
#include <stdio.h>
#include <string.h>
static int fails;
static void ok(int c, const char *w) { printf(" %s %s\n", c ? "ok " : "FAIL", w); if (!c) fails++; }
int main(void) {
PsxGameModeEnv e;
const char *why;
memset(&e, 0, sizeof e); e.cli_game_mode = -1;
ok(!psx_game_mode_detect(&e, &why) && !strcmp(why, "desktop"), "desktop: off");
e.xdg_current_desktop = "gamescope";
ok(psx_game_mode_detect(&e, &why), "XDG_CURRENT_DESKTOP=gamescope: on");
e.xdg_current_desktop = "KDE"; e.gamescope_display = "gamescope-0";
ok(psx_game_mode_detect(&e, &why), "GAMESCOPE_WAYLAND_DISPLAY: on");
e.gamescope_display = NULL; e.steam_gamepad_ui = "1";
ok(psx_game_mode_detect(&e, &why), "SteamGamepadUI=1: on");
e.steam_gamepad_ui = "0";
ok(!psx_game_mode_detect(&e, &why), "SteamGamepadUI=0: off");
e.steam_gamepad_ui = "1"; e.psx_game_mode = "0";
ok(!psx_game_mode_detect(&e, &why), "PSX_GAME_MODE=0 beats detection");
e.psx_game_mode = "1"; e.steam_gamepad_ui = NULL;
ok(psx_game_mode_detect(&e, &why), "PSX_GAME_MODE=1 on a desktop");
e.cli_game_mode = 0;
ok(!psx_game_mode_detect(&e, &why), "--no-game-mode beats PSX_GAME_MODE");
e.cli_game_mode = 1; e.psx_game_mode = "0";
ok(psx_game_mode_detect(&e, &why) && !strcmp(why, "--game-mode"), "--game-mode wins");
ok(!psx_game_mode_detect(NULL, &why), "NULL env is safe");
printf("%s\n", fails ? "FAILED" : "passed");
return fails ? 1 : 0;
}