From e75f2c3942137e10e87c04c3cef97bc1562fbf8c Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Tue, 28 Jan 2025 18:36:47 -0500 Subject: [PATCH] Support setting the platform profile The platform profile lives in /sys/firmware/acpi/platform_profile. The desiredprof and defaultprof options correspond to the values for the platform profile set when gamescope begins and ends, respectively. HACK: The platform profile may restrict what values the governor can take, so we choose to set the governor before the platform profile in order to store the correct default governor, and restore the platform profile before the governor. This is done to maximize correctness after restoration, but it can cause issues if the previous platform profile restricts the governor. TODO: Save all the state we care about before any of it is changed. In thsi case, we should set (and restore) the platform profile before the governor. --- common/common-profile.c | 78 +++++++++++++++++ common/common-profile.h | 44 ++++++++++ common/meson.build | 1 + daemon/gamemode-config.c | 25 +++++- daemon/gamemode-config.h | 2 + daemon/gamemode-context.c | 66 +++++++++++++++ .../com.feralinteractive.GameMode.policy.in | 12 +++ data/polkit/rules.d/gamemode.rules.in | 3 +- example/gamemode.ini | 5 ++ util/meson.build | 15 ++++ util/platprofctl.c | 84 +++++++++++++++++++ 11 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 common/common-profile.c create mode 100644 common/common-profile.h create mode 100644 util/platprofctl.c 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; +}