Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ 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 per-game override files. Setting `GAMEMODE_CONFIG=<name>` in a game's environment causes the daemon to load `gamemode.d/<name>.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:

```
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 `<name>` 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

Expand Down
198 changes: 185 additions & 13 deletions daemon/gamemode-context.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ POSSIBILITY OF SUCH DAMAGE.
#include "build-config.h"

#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <ini.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
Expand All @@ -54,17 +56,28 @@ POSSIBILITY OF SUCH DAMAGE.
#include <systemd/sd-daemon.h> /* TODO: Move usage to gamemode-dbus.c */
#include <unistd.h>

/**
* Per-game override values parsed from gamemode.d/<name>.ini files,
* selected by a GAMEMODE_CONFIG=<name> 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 {
char pin_cores[64]; /**<Raw value string: "no", "yes", "0-15", ... ("" = key absent) */
};

/**
* The GameModeClient encapsulates the remote connection, providing a list
* form to contain the pid and credentials.
*/
struct GameModeClient {
_Atomic int refcount; /**<Allow outside usage */
pid_t pid; /**< Process ID */
pid_t requester; /**< Process ID that requested it */
struct GameModeClient *next; /**<Next client in the list */
char executable[PATH_MAX]; /**<Process executable */
time_t timestamp; /**<When was the client registered */
_Atomic int refcount; /**<Allow outside usage */
pid_t pid; /**< Process ID */
pid_t requester; /**< Process ID that requested it */
struct GameModeClient *next; /**<Next client in the list */
char executable[PATH_MAX]; /**<Process executable */
time_t timestamp; /**<When was the client registered */
struct GameModeClientOverrides overrides; /**<Per-game config values */
};

enum GameModeGovernor {
Expand Down Expand Up @@ -116,6 +129,138 @@ struct GameModeContext {
} reaper;
};

/**
* Search /proc/<pid>/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';
}

return 1;
}

/**
* Load per-game override files gamemode.d/<name>.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));

/* Resolve the user's config dir: $XDG_CONFIG_HOME or ~/.config */
char user_path[PATH_MAX] = { 0 };
const char *xdg = getenv("XDG_CONFIG_HOME");
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(paths[i], "re");
if (!f)
continue;

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 of %s\n", name, err, paths[i]);
continue;
}

LOG_MSG("Per-game config [%s]: loaded %s\n", name, paths[i]);
loaded = true;
}

if (!loaded)
LOG_MSG("Per-game config [%s]: no files found\n", name);

return loaded;
}

/**
* 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->pin_cores[0] &&
(strcmp(ov->pin_cores, "no") == 0 || strcmp(ov->pin_cores, "false") == 0 ||
strcmp(ov->pin_cores, "0") == 0);
}

static GameModeContext instance = { 0 };

/**
Expand Down Expand Up @@ -761,7 +906,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 */);
Expand All @@ -772,8 +918,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;
}
Expand Down Expand Up @@ -834,6 +983,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=<name> in the
* client's environment selects gamemode.d/<name>.ini override files */
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.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);
}
}
}

/* Check our blacklist and whitelist */
if (!config_get_client_whitelisted(self->config, executable)) {
LOG_MSG("Client [%s] was rejected (not in whitelist)\n", executable);
Expand All @@ -847,7 +1014,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);
Expand All @@ -863,7 +1031,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);
Expand Down Expand Up @@ -1173,8 +1341,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);
}
Expand Down Expand Up @@ -1205,7 +1377,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);
Expand Down
7 changes: 7 additions & 0 deletions example/gamemode.ini
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ disable_splitlock=1
;amd_x3d_mode_desired=frequency
;amd_x3d_mode_default=cache

; Per-game configuration:
; A client may set GAMEMODE_CONFIG=<name> in its environment (e.g. via Steam launch options) to load
; gamemode.d/<name>.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.

[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
Expand Down