Skip to content
Draft
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
34 changes: 28 additions & 6 deletions daemon/gamemode-context.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ struct GameModeContext {

char initial_profile[64];
enum GameModeProfile current_profile;
GameModePowerProfileHold *profile_hold;

struct GameModeGPUInfo *stored_gpu; /**<Stored GPU info for the current GPU */
struct GameModeGPUInfo *target_gpu; /**<Target GPU info for the current GPU */
Expand Down Expand Up @@ -423,28 +424,49 @@ static int game_mode_set_profile(GameModeContext *self, enum GameModeProfile pro
return 0;
}

if (!profile_exists()) {
LOG_MSG("Setting platform profile unsupported; skipping\n");
return 0;
}

const char *prof_str = NULL;
char prof_config_str[CONFIG_VALUE_MAX] = { 0 };
switch (prof) {
case GAME_MODE_PROFILE_DEFAULT:
if (self->profile_hold) {
game_mode_destroy_power_profile_hold(self->profile_hold);
self->profile_hold = NULL;
self->current_profile = prof;
return 0;
}
config_get_default_profile(self->config, prof_config_str);
prof_str = prof_config_str[0] != '\0' ? prof_config_str : self->initial_profile;
break;

case GAME_MODE_PROFILE_DESIRED:
case GAME_MODE_PROFILE_DESIRED: {
config_get_desired_profile(self->config, prof_config_str);
prof_str = prof_config_str[0] != '\0' ? prof_config_str : "performance";

char default_profile[CONFIG_VALUE_MAX] = { 0 };
config_get_default_profile(self->config, default_profile);
if (default_profile[0] == '\0' &&
(strcmp(prof_str, "performance") == 0 || strcmp(prof_str, "power-saver") == 0)) {
enum GameModePowerProfileHoldResult result =
game_mode_create_power_profile_hold(prof_str, &self->profile_hold);
if (result == GAME_MODE_POWER_PROFILE_HELD) {
self->current_profile = prof;
return 0;
}
if (result == GAME_MODE_POWER_PROFILE_FAILED)
return -1;
}
break;
}

default:
assert(!"Invalid platform profile requested");
}

if (!profile_exists()) {
LOG_MSG("Setting platform profile unsupported; skipping\n");
return 0;
}

const char *const exec_args[] = {
"pkexec", LIBEXECDIR "/platprofctl", "set", prof_str, NULL,
};
Expand Down
149 changes: 149 additions & 0 deletions daemon/gamemode-dbus.c
Original file line number Diff line number Diff line change
Expand Up @@ -832,3 +832,152 @@ void game_mode_destroy_idle_inhibitor(GameModeIdleInhibitor *inhibitor)
sd_bus_unrefp(&inhibitor->bus);
free(inhibitor);
}

struct GameModePowerProfileHold {
sd_bus *bus;
uint32_t cookie;
const char *destination;
const char *path;
const char *interface;
};

struct PowerProfilesEndpoint {
const char *destination;
const char *path;
const char *interface;
};

static const struct PowerProfilesEndpoint power_profiles_endpoints[] = {
{
"org.freedesktop.UPower.PowerProfiles",
"/org/freedesktop/UPower/PowerProfiles",
"org.freedesktop.UPower.PowerProfiles",
},
{
"net.hadess.PowerProfiles",
"/net/hadess/PowerProfiles",
"net.hadess.PowerProfiles",
},
};

static bool power_profiles_api_unavailable(const sd_bus_error *error)
{
return sd_bus_error_has_name(error, "org.freedesktop.DBus.Error.ServiceUnknown") ||
sd_bus_error_has_name(error, "org.freedesktop.DBus.Error.NameHasNoOwner") ||
sd_bus_error_has_name(error, "org.freedesktop.DBus.Error.UnknownObject") ||
sd_bus_error_has_name(error, "org.freedesktop.DBus.Error.UnknownInterface") ||
sd_bus_error_has_name(error, "org.freedesktop.DBus.Error.UnknownMethod");
}

enum GameModePowerProfileHoldResult game_mode_create_power_profile_hold(
const char *profile, GameModePowerProfileHold **hold)
{
sd_bus *bus_local = NULL;
*hold = NULL;

int ret = sd_bus_open_system(&bus_local);
if (ret < 0) {
LOG_ERROR("Could not connect to system bus for power profile hold: %s\n", strerror(-ret));
return GAME_MODE_POWER_PROFILE_FAILED;
}

for (size_t i = 0; i < sizeof(power_profiles_endpoints) / sizeof(power_profiles_endpoints[0]);
i++) {
const struct PowerProfilesEndpoint *endpoint = &power_profiles_endpoints[i];
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *reply = NULL;

ret = sd_bus_call_method(bus_local,
endpoint->destination,
endpoint->path,
endpoint->interface,
"HoldProfile",
&error,
&reply,
"sss",
profile,
"GameMode is active",
"com.feralinteractive.GameMode");
if (ret < 0) {
bool unavailable = power_profiles_api_unavailable(&error);
if (!unavailable) {
LOG_ERROR("Failed to hold power profile through %s: %s\n",
endpoint->destination,
error.message ? error.message : strerror(-ret));
}
sd_bus_message_unref(reply);
sd_bus_error_free(&error);
if (unavailable)
continue;

sd_bus_close(bus_local);
sd_bus_unref(bus_local);
return GAME_MODE_POWER_PROFILE_FAILED;
}

uint32_t cookie = 0;
ret = sd_bus_message_read(reply, "u", &cookie);
sd_bus_message_unref(reply);
sd_bus_error_free(&error);
if (ret <= 0) {
LOG_ERROR("Invalid HoldProfile response from %s: %s\n",
endpoint->destination,
ret < 0 ? strerror(-ret) : "missing hold cookie");
sd_bus_close(bus_local);
sd_bus_unref(bus_local);
return GAME_MODE_POWER_PROFILE_FAILED;
}

GameModePowerProfileHold *profile_hold = malloc(sizeof(*profile_hold));
if (!profile_hold) {
sd_bus_close(bus_local);
sd_bus_unref(bus_local);
return GAME_MODE_POWER_PROFILE_FAILED;
}

profile_hold->bus = bus_local;
profile_hold->cookie = cookie;
profile_hold->destination = endpoint->destination;
profile_hold->path = endpoint->path;
profile_hold->interface = endpoint->interface;
*hold = profile_hold;
LOG_MSG("Held power profile %s through %s\n", profile, endpoint->destination);
return GAME_MODE_POWER_PROFILE_HELD;
}

sd_bus_close(bus_local);
sd_bus_unref(bus_local);
LOG_MSG("Power profiles D-Bus API unavailable; using platform profile directly\n");
return GAME_MODE_POWER_PROFILE_UNAVAILABLE;
}

void game_mode_destroy_power_profile_hold(GameModePowerProfileHold *hold)
{
if (!hold)
return;

sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *reply = NULL;
int ret = sd_bus_call_method(hold->bus,
hold->destination,
hold->path,
hold->interface,
"ReleaseProfile",
&error,
&reply,
"u",
hold->cookie);
if (ret < 0) {
LOG_MSG("Failed to release power profile hold through %s; closing connection: %s\n",
hold->destination,
error.message ? error.message : strerror(-ret));
} else {
LOG_MSG("Released power profile hold through %s\n", hold->destination);
}

sd_bus_message_unref(reply);
sd_bus_error_free(&error);
sd_bus_close(hold->bus);
sd_bus_unref(hold->bus);
free(hold);
}
173 changes: 173 additions & 0 deletions daemon/gamemode-power-profile-tests.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*

Copyright (c) 2026, the GameMode contributors
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Feral Interactive nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

*/

#include "gamemode.h"

#ifdef USE_ELOGIND
#include <elogind/sd-bus.h>
#else
#include <systemd/sd-bus.h>
#endif

#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

/* Exercise the real hold implementation without touching the host's system bus. */
enum TestScenario { MODERN, LEGACY, ABSENT, DENIED, EMPTY_REPLY, BAD_REPLY, BUS_DOWN };
static enum TestScenario scenario;
static int holds, releases, closes, unrefs;
static const char *held_destination;
static char bus_token, reply_token;

bool get_use_syslog(void)
{
return false;
}

int __wrap_sd_bus_open_system(sd_bus **bus)
{
if (scenario == BUS_DOWN)
return -ECONNREFUSED;
*bus = (sd_bus *)&bus_token;
return 0;
}

void __wrap_sd_bus_close(sd_bus *bus)
{
assert(bus == (sd_bus *)&bus_token);
closes++;
}

sd_bus *__wrap_sd_bus_unref(sd_bus *bus)
{
assert(bus == (sd_bus *)&bus_token);
unrefs++;
return NULL;
}

sd_bus_message *__wrap_sd_bus_message_unref(sd_bus_message *message)
{
assert(!message || message == (sd_bus_message *)&reply_token);
return NULL;
}

int __wrap_sd_bus_call_method(sd_bus *bus, const char *destination, const char *path,
const char *interface, const char *member, sd_bus_error *error,
sd_bus_message **reply, const char *types, ...)
{
assert(bus == (sd_bus *)&bus_token);
assert(strcmp(destination, interface) == 0);
assert(strcmp(path,
destination[0] == 'o' ? "/org/freedesktop/UPower/PowerProfiles"
: "/net/hadess/PowerProfiles") == 0);
va_list args;
va_start(args, types);
if (strcmp(member, "HoldProfile") == 0) {
holds++;
assert(strcmp(types, "sss") == 0);
assert(strcmp(va_arg(args, const char *), "performance") == 0);
assert(strcmp(va_arg(args, const char *), "GameMode is active") == 0);
assert(strcmp(va_arg(args, const char *), "com.feralinteractive.GameMode") == 0);
va_end(args);
if (scenario == ABSENT || (scenario == LEGACY && holds == 1)) {
sd_bus_error_set_const(error, SD_BUS_ERROR_SERVICE_UNKNOWN, "No profile service");
return -ENOENT;
}
if (scenario == DENIED) {
sd_bus_error_set_const(error, SD_BUS_ERROR_ACCESS_DENIED, "Not authorized");
return -EACCES;
}
held_destination = destination;
} else {
releases++;
assert(strcmp(member, "ReleaseProfile") == 0);
assert(strcmp(types, "u") == 0);
assert(va_arg(args, uint32_t) == 42);
assert(strcmp(destination, held_destination) == 0);
va_end(args);
}
*reply = (sd_bus_message *)&reply_token;
return 1;
}

int __wrap_sd_bus_message_read(sd_bus_message *message, const char *types, ...)
{
assert(message == (sd_bus_message *)&reply_token);
assert(strcmp(types, "u") == 0);
if (scenario == EMPTY_REPLY)
return 0;
if (scenario == BAD_REPLY)
return -EBADMSG;
va_list args;
va_start(args, types);
*va_arg(args, uint32_t *) = 42;
va_end(args);
return 1;
}

static void check(enum TestScenario test, enum GameModePowerProfileHoldResult expected,
int expected_holds)
{
scenario = test;
holds = releases = closes = unrefs = 0;
held_destination = NULL;
GameModePowerProfileHold *hold = NULL;
assert(game_mode_create_power_profile_hold("performance", &hold) == expected);
assert(holds == expected_holds);
if (expected == GAME_MODE_POWER_PROFILE_HELD) {
assert(hold);
assert(closes == 0 && unrefs == 0);
game_mode_destroy_power_profile_hold(hold);
assert(releases == 1);
} else {
assert(!hold);
assert(releases == 0);
}
assert(closes == (test == BUS_DOWN ? 0 : 1));
assert(unrefs == closes);
}

int main(void)
{
check(MODERN, GAME_MODE_POWER_PROFILE_HELD, 1);
check(LEGACY, GAME_MODE_POWER_PROFILE_HELD, 2);
check(ABSENT, GAME_MODE_POWER_PROFILE_UNAVAILABLE, 2);
check(DENIED, GAME_MODE_POWER_PROFILE_FAILED, 1);
check(EMPTY_REPLY, GAME_MODE_POWER_PROFILE_FAILED, 1);
check(BAD_REPLY, GAME_MODE_POWER_PROFILE_FAILED, 1);
check(BUS_DOWN, GAME_MODE_POWER_PROFILE_FAILED, 0);
game_mode_destroy_power_profile_hold(NULL);
puts("Power profile hold lifecycle and error cases passed");
return 0;
}
Loading