diff --git a/docs/GAME_MODE.md b/docs/GAME_MODE.md new file mode 100644 index 00000000..31c702eb --- /dev/null +++ b/docs/GAME_MODE.md @@ -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`. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index ac311475..743373e9 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 diff --git a/runtime/include/game_mode.h b/runtime/include/game_mode.h new file mode 100644 index 00000000..110492a3 --- /dev/null +++ b/runtime/include/game_mode.h @@ -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 diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index af0abbde..7337b1ee 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -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 diff --git a/runtime/src/game_mode.c b/runtime/src/game_mode.c new file mode 100644 index 00000000..6aa32499 --- /dev/null +++ b/runtime/src/game_mode.c @@ -0,0 +1,34 @@ +/* game_mode.c -- see game_mode.h. */ +#include "game_mode.h" + +#include + +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; +} diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index de26182c..a82906be 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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); diff --git a/runtime/tests/test_game_mode.c b/runtime/tests/test_game_mode.c new file mode 100644 index 00000000..f2df56e9 --- /dev/null +++ b/runtime/tests/test_game_mode.c @@ -0,0 +1,34 @@ +/* game_mode: Steam Deck / gamescope detection and its precedence. */ +#include "game_mode.h" + +#include +#include + +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; +}