From fbf9c7b432a99422eaf5e93d1b4eb490b8c0acb1 Mon Sep 17 00:00:00 2001 From: Matthew Salazar Date: Wed, 5 Aug 2026 15:12:08 +0100 Subject: [PATCH 1/4] Add per-game configuration for pinning cpu --- daemon/gamemode-context.c | 179 +++++++++++++++++++++++++++++++++++--- 1 file changed, 166 insertions(+), 13 deletions(-) diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index f0524936..71dd04ba 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -45,7 +45,9 @@ POSSIBILITY OF SUCH DAMAGE. #include "build-config.h" #include +#include #include +#include #include #include #include @@ -54,17 +56,29 @@ POSSIBILITY OF SUCH DAMAGE. #include /* TODO: Move usage to gamemode-dbus.c */ #include +/** + * Per-game override values parsed from $XDG_CONFIG_HOME/gamemode.d/.ini, + * selected by a GAMEMODE_CONFIG= variable in the client's environment. + * Only per-client optimisation keys are recognised here; global machine state + * (governor, splitlock, GPU, scripts) remains owned by the main config. + */ +struct GameModeClientOverrides { + bool have_pin_cores; /**/environ for KEY= and copy its value into out. + * environ is a sequence of NUL-terminated "KEY=VALUE" strings. + * Returns true if found. + */ +static bool game_mode_client_env_value(pid_t pid, const char *key, char *out, size_t outlen) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/environ", pid); + + FILE *f = fopen(path, "re"); + if (!f) + return false; + + bool found = false; + size_t keylen = strlen(key); + char *entry = NULL; + size_t cap = 0; + + while (getdelim(&entry, &cap, '\0', f) != -1) { + if (strncmp(entry, key, keylen) == 0 && entry[keylen] == '=') { + strncpy(out, entry + keylen + 1, outlen - 1); + out[outlen - 1] = '\0'; + found = true; + break; + } + } + + free(entry); + fclose(f); + return found; +} + +/** + * A per-game config name may only contain [A-Za-z0-9_-]. Anything else + * (slashes, dots, etc.) is rejected so the name can never traverse paths. + */ +static bool game_mode_config_name_valid(const char *name) +{ + if (!name || !name[0] || strlen(name) > 64) + return false; + for (const char *p = name; *p; p++) { + if (!isalnum((unsigned char)*p) && *p != '_' && *p != '-') + return false; + } + return true; +} + +/** + * inih callback: record only [cpu] pin_cores, ignore everything else. + * Returning 1 continues parsing. + */ +static int overrides_ini_handler(void *user, const char *section, const char *name, + const char *value) +{ + struct GameModeClientOverrides *ov = user; + + if (strcmp(section, "cpu") == 0 && strcmp(name, "pin_cores") == 0) { + strncpy(ov->pin_cores, value, sizeof(ov->pin_cores) - 1); + ov->pin_cores[sizeof(ov->pin_cores) - 1] = '\0'; + ov->have_pin_cores = true; + } + + return 1; +} + +/** + * Load $XDG_CONFIG_HOME/gamemode.d/.ini (or ~/.config/gamemode.d/) into ov. + * Returns true if the file exists and is parsed. + */ +static bool game_mode_load_client_overrides(const char *name, struct GameModeClientOverrides *ov) +{ + memset(ov, 0, sizeof(*ov)); + + char path[PATH_MAX]; + const char *xdg = getenv("XDG_CONFIG_HOME"); + if (xdg) { + snprintf(path, sizeof(path), "%s/gamemode.d/%s.ini", xdg, name); + } else { + const char *home = getenv("HOME"); + if (!home) + return false; + snprintf(path, sizeof(path), "%s/.config/gamemode.d/%s.ini", home, name); + } + + FILE *f = fopen(path, "re"); + if (!f) { + LOG_MSG("Per-game config [%s]: no file at %s\n", name, path); + return false; + } + + int err = ini_parse_file(f, overrides_ini_handler, ov); + fclose(f); + + if (err) { + LOG_ERROR("Per-game config [%s]: parse error on line %d\n", name, err); + return false; + } + return true; +} + +/** + * Returns true if a per-game config disables core pinning for this client. + * Accepts the same false spellings as the main config. + */ +static bool client_pinning_disabled(const struct GameModeClientOverrides *ov) +{ + return ov && ov->have_pin_cores && + (strcmp(ov->pin_cores, "no") == 0 || strcmp(ov->pin_cores, "false") == 0 || + strcmp(ov->pin_cores, "0") == 0); +} + static GameModeContext instance = { 0 }; /** @@ -761,7 +887,8 @@ GameModeClient *game_mode_context_lookup_client(GameModeContext *self, pid_t cli return found; } -static int game_mode_apply_client_optimisations(GameModeContext *self, pid_t client) +static int game_mode_apply_client_optimisations(GameModeContext *self, pid_t client, + const struct GameModeClientOverrides *ov) { /* Store current renice and apply */ game_mode_apply_renice(self, client, 0 /* expect zero value to start with */); @@ -772,8 +899,11 @@ static int game_mode_apply_client_optimisations(GameModeContext *self, pid_t cli /* Apply scheduler policies */ game_mode_apply_scheduling(self, client); - /* Apply core pinning */ - game_mode_apply_core_pinning(self->cpu, client, false); + /* Apply core pinning, unless a per-game config disables it for this client */ + if (client_pinning_disabled(ov)) + LOG_MSG("Per-game config: core pinning disabled for client %d\n", client); + else + game_mode_apply_core_pinning(self->cpu, client, false); return 0; } @@ -834,6 +964,24 @@ int game_mode_context_register(GameModeContext *self, pid_t client, pid_t reques if (!executable) goto error_cleanup; + /* Per-game config discovery: an explicit GAMEMODE_CONFIG= in the + * client's environment selects ~/.config/gamemode.d/.ini */ + struct GameModeClientOverrides ov; + memset(&ov, 0, sizeof(ov)); + { + char cfgname[80]; + if (game_mode_client_env_value(client, "GAMEMODE_CONFIG", cfgname, sizeof(cfgname))) { + if (!game_mode_config_name_valid(cfgname)) { + LOG_ERROR("Per-game config: invalid name in GAMEMODE_CONFIG, ignored\n"); + } else if (game_mode_load_client_overrides(cfgname, &ov)) { + if (ov.have_pin_cores) + LOG_MSG("Per-game config loaded [%s]: pin_cores=%s\n", cfgname, ov.pin_cores); + else + LOG_MSG("Per-game config loaded [%s]: no overridable keys\n", cfgname); + } + } + } + /* Check our blacklist and whitelist */ if (!config_get_client_whitelisted(self->config, executable)) { LOG_MSG("Client [%s] was rejected (not in whitelist)\n", executable); @@ -847,7 +995,8 @@ int game_mode_context_register(GameModeContext *self, pid_t client, pid_t reques GameModeClient *cl = game_mode_client_new(client, executable, requester); if (!cl) goto error_cleanup; - free(executable); /* we're now done with memory */ + cl->overrides = ov; /* attach per-game overrides to the client */ + free(executable); /* we're now done with memory */ /* Begin a write lock now to insert our new client at list start */ pthread_rwlock_wrlock(&self->rwlock); @@ -863,7 +1012,7 @@ int game_mode_context_register(GameModeContext *self, pid_t client, pid_t reques game_mode_context_enter(self); } - game_mode_apply_client_optimisations(self, client); + game_mode_apply_client_optimisations(self, client, &cl->overrides); /* Unlock now we're done applying optimisations */ pthread_rwlock_unlock(&self->rwlock); @@ -1173,8 +1322,12 @@ static void game_mode_reapply_core_pinning_internal(GameModeContext *self) { pthread_rwlock_wrlock(&self->rwlock); if (game_mode_context_num_clients(self)) { - for (GameModeClient *cl = self->client; cl; cl = cl->next) + for (GameModeClient *cl = self->client; cl; cl = cl->next) { + /* Respect per-game configs which disable pinning */ + if (client_pinning_disabled(&cl->overrides)) + continue; game_mode_apply_core_pinning(self->cpu, cl->pid, true); + } } pthread_rwlock_unlock(&self->rwlock); } @@ -1205,7 +1358,7 @@ static void game_mode_reload_config_internal(GameModeContext *self) game_mode_context_enter(self); for (GameModeClient *cl = self->client; cl; cl = cl->next) - game_mode_apply_client_optimisations(self, cl->pid); + game_mode_apply_client_optimisations(self, cl->pid, &cl->overrides); } pthread_rwlock_unlock(&self->rwlock); From daba445832fef079309c1665fc4c7358f188375b Mon Sep 17 00:00:00 2001 From: Matthew Salazar Date: Wed, 5 Aug 2026 15:18:34 +0100 Subject: [PATCH 2/4] Document per-game configuration overrides --- README.md | 22 ++++++++++++++++++++++ example/gamemode.ini | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 3dbfe044..a35ab721 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,28 @@ Configuration files are loaded and merged from the following directories, from h 3. `/etc/` 4. `/usr/share/gamemode/` +### Per-game configuration + +In addition to the global configuration, individual clients can opt into a per-game override file. Setting `GAMEMODE_CONFIG=` in a game's +environment causes the daemon to load `$XDG_CONFIG_HOME/gamemode.d/.ini` (falling back to `~/.config/gamemode.d/.ini`) for that client only. + +For example, set the following Steam launch options: + +``` +GAMEMODE_CONFIG=eldenring gamemoderun %command% +``` + +And create `~/.config/gamemode.d/eldenring.ini` containing: + +```ini +[cpu] +pin_cores=no +``` + +This will disable core pinning for that game while all other clients and all global optimisations behave as configured normally. + +The `` may only contain alphanumeric characters, `-` and `_`. Per-game files can only override per-client optimisations; global machine state (governor, split lock mitigation, GPU settings, custom scripts) remains controlled by the main configuration. Currently the only recognised key is `pin_cores` under `[cpu]`, where a value of `no`, `false` or `0` disables core pinning for that client. + --- ## Note for Hybrid GPU users diff --git a/example/gamemode.ini b/example/gamemode.ini index 67cf0887..df208465 100644 --- a/example/gamemode.ini +++ b/example/gamemode.ini @@ -113,6 +113,12 @@ disable_splitlock=1 ;amd_x3d_mode_desired=frequency ;amd_x3d_mode_default=cache +; Per-game configuration: +; A client may set GAMEMODE_CONFIG= in its environment (e.g. via Steam launch options) to load +; $XDG_CONFIG_HOME/gamemode.d/.ini for that client only. Currently only [cpu] pin_cores is recognised there; a +; value of "no" disables core pinning for that client while leaving other clients and all global optimisations +; unaffected. + [supervisor] ; This section controls the new gamemode functions gamemode_request_start_for and gamemode_request_end_for ; The whilelist and blacklist control which supervisor programs are allowed to make the above requests From b834ac8a96eba904eb5d052320ac6c105189d427 Mon Sep 17 00:00:00 2001 From: Matthew Salazar Date: Thu, 6 Aug 2026 12:19:16 +0100 Subject: [PATCH 3/4] Remove redundant have_pin_cores flag --- daemon/gamemode-context.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index 71dd04ba..c6536d8d 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -63,8 +63,7 @@ POSSIBILITY OF SUCH DAMAGE. * (governor, splitlock, GPU, scripts) remains owned by the main config. */ struct GameModeClientOverrides { - bool have_pin_cores; /**pin_cores, value, sizeof(ov->pin_cores) - 1); ov->pin_cores[sizeof(ov->pin_cores) - 1] = '\0'; - ov->have_pin_cores = true; } return 1; @@ -237,7 +235,7 @@ static bool game_mode_load_client_overrides(const char *name, struct GameModeCli */ static bool client_pinning_disabled(const struct GameModeClientOverrides *ov) { - return ov && ov->have_pin_cores && + return ov && ov->pin_cores[0] && (strcmp(ov->pin_cores, "no") == 0 || strcmp(ov->pin_cores, "false") == 0 || strcmp(ov->pin_cores, "0") == 0); } @@ -974,7 +972,7 @@ int game_mode_context_register(GameModeContext *self, pid_t client, pid_t reques if (!game_mode_config_name_valid(cfgname)) { LOG_ERROR("Per-game config: invalid name in GAMEMODE_CONFIG, ignored\n"); } else if (game_mode_load_client_overrides(cfgname, &ov)) { - if (ov.have_pin_cores) + if (ov.pin_cores[0]) LOG_MSG("Per-game config loaded [%s]: pin_cores=%s\n", cfgname, ov.pin_cores); else LOG_MSG("Per-game config loaded [%s]: no overridable keys\n", cfgname); From 209c1b244f4fd84bc59ebbaa233ea82c27b541de Mon Sep 17 00:00:00 2001 From: Matthew Salazar Date: Thu, 6 Aug 2026 12:26:27 +0100 Subject: [PATCH 4/4] Search all config locations for per-game overrides --- README.md | 5 +-- daemon/gamemode-context.c | 69 +++++++++++++++++++++++++-------------- example/gamemode.ini | 3 +- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a35ab721..63e96634 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ Configuration files are loaded and merged from the following directories, from h ### Per-game configuration -In addition to the global configuration, individual clients can opt into a per-game override file. Setting `GAMEMODE_CONFIG=` in a game's -environment causes the daemon to load `$XDG_CONFIG_HOME/gamemode.d/.ini` (falling back to `~/.config/gamemode.d/.ini`) for that client only. +In addition to the global configuration, individual clients can opt into per-game override files. Setting `GAMEMODE_CONFIG=` in a game's environment causes the daemon to load `gamemode.d/.ini` for that client only. Per-game config files are checked in the same locations as the default: `/usr/share/gamemode/gamemode.d/`, `/etc/gamemode.d/`, then `$XDG_CONFIG_HOME/gamemode.d/` (falling back to `~/.config/gamemode.d/`). Values in later files override earlier ones. For example, set the following Steam launch options: @@ -71,6 +70,8 @@ This will disable core pinning for that game while all other clients and all glo The `` may only contain alphanumeric characters, `-` and `_`. Per-game files can only override per-client optimisations; global machine state (governor, split lock mitigation, GPU settings, custom scripts) remains controlled by the main configuration. Currently the only recognised key is `pin_cores` under `[cpu]`, where a value of `no`, `false` or `0` disables core pinning for that client. +Per-game files are read once, when the client registers; unlike the main configuration files they are not watched for changes, so edits apply to the next game launch. + --- ## Note for Hybrid GPU users diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index c6536d8d..6857cca8 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -57,7 +57,7 @@ POSSIBILITY OF SUCH DAMAGE. #include /** - * Per-game override values parsed from $XDG_CONFIG_HOME/gamemode.d/.ini, + * Per-game override values parsed from gamemode.d/.ini files, * selected by a GAMEMODE_CONFIG= variable in the client's environment. * Only per-client optimisation keys are recognised here; global machine state * (governor, splitlock, GPU, scripts) remains owned by the main config. @@ -195,38 +195,59 @@ static int overrides_ini_handler(void *user, const char *section, const char *na } /** - * Load $XDG_CONFIG_HOME/gamemode.d/.ini (or ~/.config/gamemode.d/) into ov. - * Returns true if the file exists and is parsed. + * Load per-game override files gamemode.d/.ini into ov. The same + * locations are searched as for the main config, in ascending priority: + * SYSCONFDIR, /etc, then $XDG_CONFIG_HOME (or ~/.config). Each file found + * is parsed into the same struct, so values in later files overwrite + * earlier ones, matching the main config's merge behaviour. + * Returns true if at least one file was loaded. */ static bool game_mode_load_client_overrides(const char *name, struct GameModeClientOverrides *ov) { memset(ov, 0, sizeof(*ov)); - char path[PATH_MAX]; + /* Resolve the user's config dir: $XDG_CONFIG_HOME or ~/.config */ + char user_path[PATH_MAX] = { 0 }; const char *xdg = getenv("XDG_CONFIG_HOME"); - if (xdg) { - snprintf(path, sizeof(path), "%s/gamemode.d/%s.ini", xdg, name); - } else { - const char *home = getenv("HOME"); - if (!home) - return false; - snprintf(path, sizeof(path), "%s/.config/gamemode.d/%s.ini", home, name); - } + const char *home = getenv("HOME"); + if (xdg) + snprintf(user_path, sizeof(user_path), "%s/gamemode.d/%s.ini", xdg, name); + else if (home) + snprintf(user_path, sizeof(user_path), "%s/.config/gamemode.d/%s.ini", home, name); + + char sys_path[PATH_MAX]; + char etc_path[PATH_MAX]; + snprintf(sys_path, sizeof(sys_path), SYSCONFDIR "/gamemode.d/%s.ini", name); + snprintf(etc_path, sizeof(etc_path), "/etc/gamemode.d/%s.ini", name); + + /* Ascending priority, matching load_config_files() */ + const char *paths[] = { sys_path, etc_path, user_path[0] ? user_path : NULL }; + + bool loaded = false; + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { + if (!paths[i]) + continue; - FILE *f = fopen(path, "re"); - if (!f) { - LOG_MSG("Per-game config [%s]: no file at %s\n", name, path); - return false; - } + FILE *f = fopen(paths[i], "re"); + if (!f) + continue; - int err = ini_parse_file(f, overrides_ini_handler, ov); - fclose(f); + int err = ini_parse_file(f, overrides_ini_handler, ov); + fclose(f); - if (err) { - LOG_ERROR("Per-game config [%s]: parse error on line %d\n", name, err); - return false; + if (err) { + LOG_ERROR("Per-game config [%s]: parse error on line %d of %s\n", name, err, paths[i]); + continue; + } + + LOG_MSG("Per-game config [%s]: loaded %s\n", name, paths[i]); + loaded = true; } - return true; + + if (!loaded) + LOG_MSG("Per-game config [%s]: no files found\n", name); + + return loaded; } /** @@ -963,7 +984,7 @@ int game_mode_context_register(GameModeContext *self, pid_t client, pid_t reques goto error_cleanup; /* Per-game config discovery: an explicit GAMEMODE_CONFIG= in the - * client's environment selects ~/.config/gamemode.d/.ini */ + * client's environment selects gamemode.d/.ini override files */ struct GameModeClientOverrides ov; memset(&ov, 0, sizeof(ov)); { diff --git a/example/gamemode.ini b/example/gamemode.ini index df208465..2fe5ba77 100644 --- a/example/gamemode.ini +++ b/example/gamemode.ini @@ -115,7 +115,8 @@ disable_splitlock=1 ; Per-game configuration: ; A client may set GAMEMODE_CONFIG= in its environment (e.g. via Steam launch options) to load -; $XDG_CONFIG_HOME/gamemode.d/.ini for that client only. Currently only [cpu] pin_cores is recognised there; a +; gamemode.d/.ini override files for that client only, searched in the same locations as this config +; (/usr/share/gamemode, /etc, then the user config dir). Currently only [cpu] pin_cores is recognised there; a ; value of "no" disables core pinning for that client while leaving other clients and all global optimisations ; unaffected.