From 1fa3d6051ae6ac27e0e92c8152be743e4cf3a2fa Mon Sep 17 00:00:00 2001 From: "Can H. Tartanoglu" Date: Thu, 13 Aug 2026 07:06:52 +0200 Subject: [PATCH 1/2] Hold power profile through power-profiles-daemon --- daemon/gamemode-context.c | 34 +++++++-- daemon/gamemode-dbus.c | 149 ++++++++++++++++++++++++++++++++++++++ daemon/gamemode.h | 9 +++ example/gamemode.ini | 6 +- 4 files changed, 190 insertions(+), 8 deletions(-) diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index f0524936..878ac20c 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -91,6 +91,7 @@ struct GameModeContext { char initial_profile[64]; enum GameModeProfile current_profile; + GameModePowerProfileHold *profile_hold; struct GameModeGPUInfo *stored_gpu; /**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, }; diff --git a/daemon/gamemode-dbus.c b/daemon/gamemode-dbus.c index b00ccb65..d41a338e 100644 --- a/daemon/gamemode-dbus.c +++ b/daemon/gamemode-dbus.c @@ -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, + strerror(-ret)); + 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); +} diff --git a/daemon/gamemode.h b/daemon/gamemode.h index e0fa201b..2532bb94 100644 --- a/daemon/gamemode.h +++ b/daemon/gamemode.h @@ -230,8 +230,17 @@ void game_mode_undo_core_pinning(const GameModeCPUInfo *info, const pid_t client * Provides an API interface for using dbus */ typedef struct GameModeIdleInhibitor GameModeIdleInhibitor; +typedef struct GameModePowerProfileHold GameModePowerProfileHold; +enum GameModePowerProfileHoldResult { + GAME_MODE_POWER_PROFILE_HELD, + GAME_MODE_POWER_PROFILE_UNAVAILABLE, + GAME_MODE_POWER_PROFILE_FAILED, +}; void game_mode_context_loop(GameModeContext *context) __attribute__((noreturn)); GameModeIdleInhibitor *game_mode_create_idle_inhibitor(void); void game_mode_destroy_idle_inhibitor(GameModeIdleInhibitor *inhibitor); +enum GameModePowerProfileHoldResult game_mode_create_power_profile_hold( + const char *profile, GameModePowerProfileHold **hold); +void game_mode_destroy_power_profile_hold(GameModePowerProfileHold *hold); void game_mode_client_registered(pid_t); void game_mode_client_unregistered(pid_t); diff --git a/example/gamemode.ini b/example/gamemode.ini index 67cf0887..0e21e0db 100644 --- a/example/gamemode.ini +++ b/example/gamemode.ini @@ -7,10 +7,12 @@ desiredgov=performance ; The default governor is used when leaving GameMode instead of restoring the original value ;defaultgov=powersave -; The desired platform profile is used when entering GameMode instead of "performance" +; The desired platform profile is used when entering GameMode instead of "performance". +; GameMode requests "performance" or "power-saver" through power-profiles-daemon when available. desiredprof=performance ; The default platform profile is used when leaving GameMode instead of restoring the original value -;defaultgov=low-power +; Setting this uses direct platform profile control instead of a power-profiles-daemon hold. +;defaultprof=low-power ; The iGPU desired governor is used when the integrated GPU is under heavy load igpu_desiredgov=powersave From 627aac2f605824d6ba71c73a6c2f7cbd197d661c Mon Sep 17 00:00:00 2001 From: "Can H. Tartanoglu" Date: Thu, 8 Oct 2026 11:05:15 +0200 Subject: [PATCH 2/2] fix: reject power profile holds without a cookie Treat an empty HoldProfile reply as a failed hold instead of retaining cookie zero. Exercise the real hold lifecycle against a fake bus so modern and legacy endpoints, missing services, denied requests and malformed replies are checked without changing the host power profile. --- daemon/gamemode-dbus.c | 4 +- daemon/gamemode-power-profile-tests.c | 173 ++++++++++++++++++++++++++ daemon/meson.build | 19 +++ 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 daemon/gamemode-power-profile-tests.c diff --git a/daemon/gamemode-dbus.c b/daemon/gamemode-dbus.c index d41a338e..76104bb4 100644 --- a/daemon/gamemode-dbus.c +++ b/daemon/gamemode-dbus.c @@ -919,10 +919,10 @@ enum GameModePowerProfileHoldResult game_mode_create_power_profile_hold( ret = sd_bus_message_read(reply, "u", &cookie); sd_bus_message_unref(reply); sd_bus_error_free(&error); - if (ret < 0) { + if (ret <= 0) { LOG_ERROR("Invalid HoldProfile response from %s: %s\n", endpoint->destination, - strerror(-ret)); + ret < 0 ? strerror(-ret) : "missing hold cookie"); sd_bus_close(bus_local); sd_bus_unref(bus_local); return GAME_MODE_POWER_PROFILE_FAILED; diff --git a/daemon/gamemode-power-profile-tests.c b/daemon/gamemode-power-profile-tests.c new file mode 100644 index 00000000..a0613278 --- /dev/null +++ b/daemon/gamemode-power-profile-tests.c @@ -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 +#else +#include +#endif + +#include +#include +#include +#include +#include + +/* 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; +} diff --git a/daemon/meson.build b/daemon/meson.build index d132d955..659943f1 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -38,3 +38,22 @@ test( gamemoded, args: ['-v'], ) + +# Keep this test on a fake bus: it must never change the host's power profile. +power_profile_tests = executable( + 'gamemode-power-profile-tests', + sources: ['gamemode-dbus.c', 'gamemode-power-profile-tests.c'], + c_args: sd_bus_args + ['-ffunction-sections', '-fdata-sections', '-UNDEBUG'], + link_args: [ + '-Wl,--gc-sections', + '-Wl,--wrap=sd_bus_open_system', + '-Wl,--wrap=sd_bus_close', + '-Wl,--wrap=sd_bus_unref', + '-Wl,--wrap=sd_bus_call_method', + '-Wl,--wrap=sd_bus_message_read', + '-Wl,--wrap=sd_bus_message_unref', + ], + dependencies: [link_daemon_common, sd_bus_dep], + include_directories: gamemoded_includes, +) +test('power profile hold lifecycle', power_profile_tests)