diff --git a/common/common-profile.c b/common/common-profile.c new file mode 100644 index 0000000..d26b4cf --- /dev/null +++ b/common/common-profile.c @@ -0,0 +1,78 @@ +/* + +Copyright (c) 2025, MithicSpirit +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. + + */ + +#define _GNU_SOURCE + +#include "common-profile.h" +#include "common-logging.h" + +/** + * Path for platform profile + */ +const char *profile_path = "/sys/firmware/acpi/platform_profile"; + +/** + * Return the current platform profile state + */ +const char *get_profile_state(void) +{ + /* Persistent profile state */ + static char profile[64] = { 0 }; + memset(profile, 0, sizeof(profile)); + + FILE *f = fopen(profile_path, "r"); + if (!f) { + LOG_ERROR("Failed to open file for read %s\n", profile_path); + return "none"; + } + + /* Grab the file length */ + fseek(f, 0, SEEK_END); + long length = ftell(f); + fseek(f, 0, SEEK_SET); + + if (length == -1) { + LOG_ERROR("Failed to seek file %s\n", profile_path); + } else { + char contents[length + 1]; + + if (fread(contents, 1, (size_t)length, f) > 0) { + strtok(contents, "\n"); + strncpy(profile, contents, sizeof(profile) - 1); + } else { + LOG_ERROR("Failed to read contents of %s\n", profile_path); + } + } + + fclose(f); + + return profile; +} diff --git a/common/common-profile.h b/common/common-profile.h new file mode 100644 index 0000000..76b0e59 --- /dev/null +++ b/common/common-profile.h @@ -0,0 +1,44 @@ +/* + +Copyright (c) 2025, MithicSpirit +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. + + */ + +#pragma once + +#include + +/** + * Path for platform profile + */ +extern const char *profile_path; + +/** + * Get the current platform profile state + */ +const char *get_profile_state(void); diff --git a/common/meson.build b/common/meson.build index 0d00b59..3408214 100644 --- a/common/meson.build +++ b/common/meson.build @@ -2,6 +2,7 @@ common_sources = [ 'common-logging.c', 'common-governors.c', + 'common-profile.c', 'common-external.c', 'common-helpers.c', 'common-gpu.c', diff --git a/daemon/gamemode-config.c b/daemon/gamemode-config.c index 59dc13b..483de6f 100644 --- a/daemon/gamemode-config.c +++ b/daemon/gamemode-config.c @@ -88,6 +88,9 @@ struct GameModeConfig { char defaultgov[CONFIG_VALUE_MAX]; char desiredgov[CONFIG_VALUE_MAX]; + char defaultprof[CONFIG_VALUE_MAX]; + char desiredprof[CONFIG_VALUE_MAX]; + char igpu_desiredgov[CONFIG_VALUE_MAX]; float igpu_power_threshold; @@ -265,6 +268,10 @@ static int inih_handler(void *user, const char *section, const char *name, const valid = get_string_value(value, self->values.defaultgov); } else if (strcmp(name, "desiredgov") == 0) { valid = get_string_value(value, self->values.desiredgov); + } else if (strcmp(name, "defaultprof") == 0) { + valid = get_string_value(value, self->values.defaultprof); + } else if (strcmp(name, "desiredprof") == 0) { + valid = get_string_value(value, self->values.desiredprof); } else if (strcmp(name, "igpu_desiredgov") == 0) { valid = get_string_value(value, self->values.igpu_desiredgov); } else if (strcmp(name, "igpu_power_threshold") == 0) { @@ -691,13 +698,29 @@ void config_get_default_governor(GameModeConfig *self, char governor[CONFIG_VALU } /* - * Get the chosen desired governor + * Get the chosen desired platform profile */ void config_get_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]) { memcpy_locked_config(self, governor, self->values.desiredgov, sizeof(self->values.desiredgov)); } +/* + * Get the chosen default platform profile + */ +void config_get_default_profile(GameModeConfig *self, char profile[CONFIG_VALUE_MAX]) +{ + memcpy_locked_config(self, profile, self->values.defaultprof, sizeof(self->values.defaultprof)); +} + +/* + * Get the chosen desired governor + */ +void config_get_desired_profile(GameModeConfig *self, char profile[CONFIG_VALUE_MAX]) +{ + memcpy_locked_config(self, profile, self->values.desiredprof, sizeof(self->values.desiredprof)); +} + /* * Get the chosen iGPU desired governor */ diff --git a/daemon/gamemode-config.h b/daemon/gamemode-config.h index d9da118..a134beb 100644 --- a/daemon/gamemode-config.h +++ b/daemon/gamemode-config.h @@ -102,6 +102,8 @@ bool config_get_inhibit_screensaver(GameModeConfig *self); long config_get_script_timeout(GameModeConfig *self); void config_get_default_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]); void config_get_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]); +void config_get_default_profile(GameModeConfig *self, char profile[CONFIG_VALUE_MAX]); +void config_get_desired_profile(GameModeConfig *self, char profile[CONFIG_VALUE_MAX]); void config_get_igpu_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]); float config_get_igpu_power_threshold(GameModeConfig *self); void config_get_soft_realtime(GameModeConfig *self, char softrealtime[CONFIG_VALUE_MAX]); diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index 9ad1367..f04dec5 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -36,6 +36,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "common-helpers.h" #include "common-logging.h" #include "common-power.h" +#include "common-profile.h" #include "gamemode.h" #include "gamemode-config.h" @@ -71,6 +72,11 @@ enum GameModeGovernor { GAME_MODE_GOVERNOR_IGPU_DESIRED, }; +enum GameModeProfile { + GAME_MODE_PROFILE_DEFAULT, + GAME_MODE_PROFILE_DESIRED, +}; + struct GameModeContext { pthread_rwlock_t rwlock; /**current_profile == prof) { + return 0; + } + + if (self->current_profile == GAME_MODE_PROFILE_DEFAULT) { + /* Read the initial platform profile so we can revert it correctly */ + const char *initial_state = get_profile_state(); + if (initial_state == NULL) { + return 0; + } + + /* store the initial platform profile */ + strncpy(self->initial_profile, initial_state, sizeof(self->initial_profile) - 1); + self->initial_profile[sizeof(self->initial_profile) - 1] = '\0'; + LOG_MSG("platform profile was initially set to [%s]\n", initial_state); + } + + const char *prof_str = NULL; + char prof_config_str[CONFIG_VALUE_MAX] = { 0 }; + switch (prof) { + case GAME_MODE_PROFILE_DEFAULT: + 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: + config_get_desired_profile(self->config, prof_config_str); + prof_str = prof_config_str[0] != '\0' ? prof_config_str : "performance"; + break; + + default: + assert(!"Invalid platform profile requested"); + } + + const char *const exec_args[] = { + "pkexec", LIBEXECDIR "/platprofctl", "set", prof_str, NULL, + }; + + LOG_MSG("Requesting update of platform profile to %s\n", prof_str); + int ret = run_external_process(exec_args, NULL, -1); + if (ret != 0) { + LOG_ERROR("Failed to update platform profile\n"); + return ret; + } + + /* Update the current govenor only if we succeed at setting govenors. */ + self->current_profile = prof; + + return 0; +} + static void game_mode_enable_igpu_optimization(GameModeContext *self) { float threshold = config_get_igpu_power_threshold(self->config); @@ -424,6 +486,8 @@ static void game_mode_context_enter(GameModeContext *self) game_mode_enable_igpu_optimization(self); } + game_mode_set_profile(self, GAME_MODE_PROFILE_DESIRED); + /* Inhibit the screensaver */ if (config_get_inhibit_screensaver(self->config)) { game_mode_destroy_idle_inhibitor(self->idle_inhibitor); @@ -471,6 +535,8 @@ static void game_mode_context_leave(GameModeContext *self) game_mode_disable_splitlock(self, false); + game_mode_set_profile(self, GAME_MODE_PROFILE_DEFAULT); + game_mode_set_governor(self, GAME_MODE_GOVERNOR_DEFAULT); game_mode_disable_igpu_optimization(self); diff --git a/data/polkit/actions/com.feralinteractive.GameMode.policy.in b/data/polkit/actions/com.feralinteractive.GameMode.policy.in index df14d15..1a2c505 100644 --- a/data/polkit/actions/com.feralinteractive.GameMode.policy.in +++ b/data/polkit/actions/com.feralinteractive.GameMode.policy.in @@ -58,4 +58,16 @@ @LIBEXECDIR@/procsysctl true + + + Modify the platform profile + Authentication is required to modify platform profile + + no + no + no + + @LIBEXECDIR@/platprofctl + true + diff --git a/data/polkit/rules.d/gamemode.rules.in b/data/polkit/rules.d/gamemode.rules.in index 0420ddc..21a209d 100644 --- a/data/polkit/rules.d/gamemode.rules.in +++ b/data/polkit/rules.d/gamemode.rules.in @@ -6,7 +6,8 @@ polkit.addRule(function (action, subject) { if ((action.id == "com.feralinteractive.GameMode.governor-helper" || action.id == "com.feralinteractive.GameMode.gpu-helper" || action.id == "com.feralinteractive.GameMode.cpu-helper" || - action.id == "com.feralinteractive.GameMode.procsys-helper") && + action.id == "com.feralinteractive.GameMode.procsys-helper" || + action.id == "com.feralinteractive.GameMode.profile-helper") && subject.isInGroup("@GAMEMODE_PRIVILEGED_GROUP@")) { return polkit.Result.YES; diff --git a/example/gamemode.ini b/example/gamemode.ini index 74a759b..45365bd 100644 --- a/example/gamemode.ini +++ b/example/gamemode.ini @@ -7,6 +7,11 @@ 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" +desiredprof=performance +; The default platform profile is used when leaving GameMode instead of restoring the original value +;defaultgov=low-power + ; The iGPU desired governor is used when the integrated GPU is under heavy load igpu_desiredgov=powersave ; Threshold to use to decide when the integrated GPU is under heavy load. diff --git a/util/meson.build b/util/meson.build index 99c8ae4..4d76c05 100644 --- a/util/meson.build +++ b/util/meson.build @@ -58,3 +58,18 @@ procsysctl = executable( install: true, install_dir: path_libexecdir, ) + +# Small target util to get and set platform profile +platprofctl_sources = [ + 'platprofctl.c', +] + +platprofctl = executable( + 'platprofctl', + sources: platprofctl_sources, + dependencies: [ + link_daemon_common, + ], + install: true, + install_dir: path_libexecdir, +) diff --git a/util/platprofctl.c b/util/platprofctl.c new file mode 100644 index 0000000..b6e5c59 --- /dev/null +++ b/util/platprofctl.c @@ -0,0 +1,84 @@ +/* + +Copyright (c) 2025, MithicSpirit +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. + + */ + +#define _GNU_SOURCE + +#include "common-logging.h" +#include "common-profile.h" + +#include + +/** + * Sets platform profile to a value + */ +static int set_profile_state(const char *value) +{ + int retval = EXIT_SUCCESS; + + FILE *f = fopen(profile_path, "w"); + if (!f) { + LOG_ERROR("Failed to open file for write %s\n", profile_path); + retval = EXIT_FAILURE; + } + + if (fprintf(f, "%s\n", value) < 0) { + LOG_ERROR("Failed to set platform profile to %s: %s", value, strerror(errno)); + retval = EXIT_FAILURE; + } + fclose(f); + + return retval; +} + +/** + * Main entry point, dispatch to the appropriate helper + */ +int main(int argc, char *argv[]) +{ + if (argc == 2 && strncmp(argv[1], "get", 3) == 0) { + printf("%s", get_profile_state()); + } else if (argc == 3 && strncmp(argv[1], "set", 3) == 0) { + const char *value = argv[2]; + + /* Must be root to set the state */ + if (geteuid() != 0) { + LOG_ERROR("This program must be run as root\n"); + return EXIT_FAILURE; + } + + return set_profile_state(value); + } else { + fprintf(stderr, "usage: platprofctl [get] [set VALUE]\n"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}