diff --git a/bootstrap.sh b/bootstrap.sh index c85ba26..de5a4b0 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -40,5 +40,10 @@ set -x sudo ninja install +# Restart polkit so we don't get pop-ups whenever we pkexec +if systemctl list-unit-files |grep -q polkit.service; then + sudo systemctl try-restart polkit +fi + # Reload systemd configuration so that it picks up the new service. systemctl --user daemon-reload diff --git a/common/common-power.c b/common/common-power.c new file mode 100644 index 0000000..7cf6047 --- /dev/null +++ b/common/common-power.c @@ -0,0 +1,165 @@ +/* + +Copyright (c) 2017-2019, Feral Interactive +Copyright (c) 2019, Intel Corporation +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-power.h" +#include "common-logging.h" + +#include +#include +#include +#include +#include +#include + +static bool read_file_in_dir(const char *dir, const char *file, char *dest, size_t n) +{ + char path[PATH_MAX]; + int ret = snprintf(path, sizeof(path), "%s/%s", dir, file); + if (ret < 0 || ret >= (int)sizeof(path)) { + LOG_ERROR("Path length overrun"); + return false; + } + + FILE *f = fopen(path, "r"); + if (!f) { + LOG_ERROR("Failed to open file for read %s\n", path); + return false; + } + + size_t read = fread(dest, 1, n, f); + + /* Close before we do any error checking */ + fclose(f); + + if (read <= 0) { + LOG_ERROR("Failed to read contents of %s: (%s)\n", path, strerror(errno)); + return false; + } + + if (read >= n) { + LOG_ERROR("File contained more data than expected %s\n", path); + return false; + } + + /* Ensure we're null terminated */ + dest[read] = '\0'; + + /* Trim whitespace off the end */ + while (read > 0 && isspace(dest[read - 1])) { + dest[read - 1] = '\0'; + read--; + } + + return true; +} + +static bool get_energy_uj(const char *rapl_name, uint32_t *energy_uj) +{ + glob_t glo = { 0 }; + static const char *path = "/sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:*"; + + /* Assert some sanity on this glob */ + if (glob(path, GLOB_NOSORT, NULL, &glo) != 0) { + LOG_ERROR("glob failed for RAPL paths: (%s)\n", strerror(errno)); + return false; + } + + /* If the glob doesn't find anything, this most likely means we don't + * have an Intel CPU or we have a kernel which does not support RAPL on + * our CPU. + */ + if (glo.gl_pathc < 1) { + LOG_ONCE(MSG, + "Intel RAPL interface not found in sysfs. " + "This is only problematic if you expected Intel iGPU " + "power threshold optimization."); + globfree(&glo); + return false; + } + + /* Walk the glob set */ + for (size_t i = 0; i < glo.gl_pathc; i++) { + char name[32]; + if (!read_file_in_dir(glo.gl_pathv[i], "name", name, sizeof(name))) { + return false; + } + + /* We're searching for the directory where the file named "name" + * contains the contents rapl_name. */ + if (strncmp(name, rapl_name, sizeof(name)) != 0) { + continue; + } + + char energy_uj_str[32]; + if (!read_file_in_dir(glo.gl_pathv[i], "energy_uj", energy_uj_str, sizeof(energy_uj_str))) { + return false; + } + + char *end = NULL; + long long energy_uj_ll = strtoll(energy_uj_str, &end, 10); + if (end == energy_uj_str) { + LOG_ERROR("Invalid energy_uj contents: %s\n", energy_uj_str); + return false; + } + + if (energy_uj_ll < 0) { + LOG_ERROR("Value of energy_uj is out of expected bounds: %lld\n", energy_uj_ll); + return false; + } + + /* Go ahead and clamp to 32 bits. We assume 32 bits later when + * taking deltas and wrapping at 32 bits is exactly what the Linux + * kernel's turbostat utility does so it's probably right. + */ + *energy_uj = (uint32_t)energy_uj_ll; + return true; + } + + /* If we got here then the CPU and Kernel support RAPL and all our file + * access has succeeded but we failed to find an entry with the right + * name. This most likely means we're asking for "uncore" but are on a + * machine that doesn't have an integrated GPU. + */ + return false; +} + +bool get_cpu_energy_uj(uint32_t *energy_uj) +{ + return get_energy_uj("core", energy_uj); +} + +bool get_igpu_energy_uj(uint32_t *energy_uj) +{ + return get_energy_uj("uncore", energy_uj); +} diff --git a/common/common-power.h b/common/common-power.h new file mode 100644 index 0000000..e92a636 --- /dev/null +++ b/common/common-power.h @@ -0,0 +1,45 @@ +/* + +Copyright (c) 2017-2019, Feral Interactive +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 +#include + +/** + * Get the amount of energy used to date by the CPU in microjoules + */ +bool get_cpu_energy_uj(uint32_t *energy_uj); + +/** + * Get the amount of energy used to date by the integrated GPU in microjoules + */ +bool get_igpu_energy_uj(uint32_t *energy_uj); diff --git a/common/meson.build b/common/meson.build index 9411900..a2e857a 100644 --- a/common/meson.build +++ b/common/meson.build @@ -6,6 +6,7 @@ common_sources = [ 'common-helpers.c', 'common-gpu.c', 'common-pidfds.c', + 'common-power.c', ] daemon_common = static_library( diff --git a/daemon/gamemode-config.c b/daemon/gamemode-config.c index 6b8f5d9..324e71b 100644 --- a/daemon/gamemode-config.c +++ b/daemon/gamemode-config.c @@ -39,6 +39,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "ini.h" #include +#include #include #include #include @@ -50,6 +51,8 @@ POSSIBILITY OF SUCH DAMAGE. /* Default value for the reaper frequency */ #define DEFAULT_REAPER_FREQ 5 +#define DEFAULT_IGPU_POWER_THRESHOLD 0.3f + /* Helper macro for defining the config variable getter */ #define DEFINE_CONFIG_GET(name) \ long config_get_##name(GameModeConfig *self) \ @@ -82,6 +85,9 @@ struct GameModeConfig { char defaultgov[CONFIG_VALUE_MAX]; char desiredgov[CONFIG_VALUE_MAX]; + char igpu_desiredgov[CONFIG_VALUE_MAX]; + float igpu_power_threshold; + char softrealtime[CONFIG_VALUE_MAX]; long renice; @@ -179,6 +185,27 @@ __attribute__((unused)) static bool get_long_value_hex(const char *value_name, c return true; } +/* + * Get a long value from a string + */ +static bool get_float_value(const char *value_name, const char *value, float *output) +{ + char *end = NULL; + float config_value = strtof(value, &end); + + if (errno == ERANGE) { + LOG_ERROR("Config: %s overflowed, given [%s]\n", value_name, value); + return false; + } else if (!(*value != '\0' && end && *end == '\0')) { + LOG_ERROR("Config: %s was invalid, given [%s]\n", value_name, value); + return false; + } else { + *output = config_value; + } + + return true; +} + /** * Simple strstr scheck * Could be expanded for wildcard or regex @@ -230,6 +257,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, "igpu_desiredgov") == 0) { + valid = get_string_value(value, self->values.igpu_desiredgov); + } else if (strcmp(name, "igpu_power_threshold") == 0) { + valid = get_float_value(name, value, &self->values.igpu_power_threshold); } else if (strcmp(name, "softrealtime") == 0) { valid = get_string_value(value, self->values.softrealtime); } else if (strcmp(name, "renice") == 0) { @@ -329,6 +360,7 @@ static void load_config_files(GameModeConfig *self) memset(&self->values, 0, sizeof(self->values)); /* Set some non-zero defaults */ + self->values.igpu_power_threshold = DEFAULT_IGPU_POWER_THRESHOLD; self->values.inhibit_screensaver = 1; /* Defaults to on */ self->values.reaper_frequency = DEFAULT_REAPER_FREQ; self->values.gpu_device = -1; /* 0 is a valid device ID so use -1 to indicate no value */ @@ -641,6 +673,35 @@ void config_get_desired_governor(GameModeConfig *self, char governor[CONFIG_VALU memcpy_locked_config(self, governor, self->values.desiredgov, sizeof(self->values.desiredgov)); } +/* + * Get the chosen iGPU desired governor + */ +void config_get_igpu_desired_governor(GameModeConfig *self, char governor[CONFIG_VALUE_MAX]) +{ + memcpy_locked_config(self, + governor, + self->values.igpu_desiredgov, + sizeof(self->values.igpu_desiredgov)); +} + +/* + * Get the chosen iGPU power threshold + */ +float config_get_igpu_power_threshold(GameModeConfig *self) +{ + float value = 0; + memcpy_locked_config(self, &value, &self->values.igpu_power_threshold, sizeof(float)); + /* Validate the threshold value */ + if (isnan(value) || value < 0) { + LOG_ONCE(ERROR, + "Configured iGPU power threshold value '%f' is invalid, ignoring iGPU default " + "governor.\n", + value); + value = FP_INFINITE; + } + return value; +} + /* * Get the chosen soft realtime behavior */ diff --git a/daemon/gamemode-config.h b/daemon/gamemode-config.h index 9aebbed..c81af5e 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_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]); long config_get_renice_value(GameModeConfig *self); long config_get_ioprio_value(GameModeConfig *self); diff --git a/daemon/gamemode-context.c b/daemon/gamemode-context.c index bb635d1..56c3335 100644 --- a/daemon/gamemode-context.c +++ b/daemon/gamemode-context.c @@ -35,6 +35,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "common-governors.h" #include "common-helpers.h" #include "common-logging.h" +#include "common-power.h" #include "gamemode.h" #include "gamemode-config.h" @@ -63,6 +64,12 @@ struct GameModeClient { time_t timestamp; /**config = config_create(); config_init(self->config); + self->current_govenor = GAME_MODE_GOVERNOR_DEFAULT; + /* Initialise the current GPU info */ game_mode_initialise_gpu(self->config, &self->stored_gpu); game_mode_initialise_gpu(self->config, &self->target_gpu); @@ -178,6 +193,151 @@ void game_mode_context_destroy(GameModeContext *self) pthread_rwlock_destroy(&self->rwlock); } +static int game_mode_set_governor(GameModeContext *self, enum GameModeGovernor gov) +{ + if (self->current_govenor == gov) { + return 0; + } + + if (self->current_govenor == GAME_MODE_GOVERNOR_DEFAULT) { + /* Read the initial governor state so we can revert it correctly */ + const char *initial_state = get_gov_state(); + if (initial_state == NULL) { + return 0; + } + + /* store the initial cpu governor mode */ + strncpy(self->initial_cpu_mode, initial_state, sizeof(self->initial_cpu_mode) - 1); + self->initial_cpu_mode[sizeof(self->initial_cpu_mode) - 1] = '\0'; + LOG_MSG("governor was initially set to [%s]\n", initial_state); + } + + char *gov_str = NULL; + char gov_config_str[CONFIG_VALUE_MAX] = { 0 }; + switch (gov) { + case GAME_MODE_GOVERNOR_DEFAULT: + config_get_default_governor(self->config, gov_config_str); + gov_str = gov_config_str[0] != '\0' ? gov_config_str : self->initial_cpu_mode; + break; + + case GAME_MODE_GOVERNOR_DESIRED: + config_get_desired_governor(self->config, gov_config_str); + gov_str = gov_config_str[0] != '\0' ? gov_config_str : "performance"; + break; + + case GAME_MODE_GOVERNOR_IGPU_DESIRED: + config_get_igpu_desired_governor(self->config, gov_config_str); + gov_str = gov_config_str[0] != '\0' ? gov_config_str : "powersave"; + break; + + default: + assert(!"Invalid governor requested"); + } + + const char *const exec_args[] = { + "/usr/bin/pkexec", LIBEXECDIR "/cpugovctl", "set", gov_str, NULL, + }; + + LOG_MSG("Requesting update of governor policy to %s\n", gov_str); + int ret = run_external_process(exec_args, NULL, -1); + if (ret != 0) { + LOG_ERROR("Failed to update cpu governor policy\n"); + return ret; + } + + /* Update the current govenor only if we succeed at setting govenors. */ + self->current_govenor = gov; + + return 0; +} + +static void game_mode_enable_igpu_optimization(GameModeContext *self) +{ + float threshold = config_get_igpu_power_threshold(self->config); + + /* There's no way the GPU is using 10000x the power. This lets us + * short-circuit if the config file specifies an invalid threshold + * and we want to disable the iGPU heuristic. + */ + if (threshold < 10000 && get_cpu_energy_uj(&self->last_cpu_energy_uj) && + get_igpu_energy_uj(&self->last_igpu_energy_uj)) { + LOG_MSG( + "Successfully queried power data for the CPU and iGPU. " + "Enabling the integrated GPU optimization"); + self->igpu_optimization_enabled = true; + } +} + +static void game_mode_disable_igpu_optimization(GameModeContext *self) +{ + self->igpu_optimization_enabled = false; +} + +static void game_mode_check_igpu_energy(GameModeContext *self) +{ + pthread_rwlock_wrlock(&self->rwlock); + + /* We only care if we're not in the default governor */ + if (self->current_govenor == GAME_MODE_GOVERNOR_DEFAULT) + goto unlock; + + if (!self->igpu_optimization_enabled) + goto unlock; + + uint32_t cpu_energy_uj, igpu_energy_uj; + if (!get_cpu_energy_uj(&cpu_energy_uj) || !get_igpu_energy_uj(&igpu_energy_uj)) { + /* We've already succeeded at getting power information once so + * failing here is possible but very unexpected. */ + self->igpu_optimization_enabled = false; + LOG_ERROR("Failed to get CPU and iGPU power data\n"); + goto unlock; + } + + /* The values we query from RAPL are in units of microjoules of energy + * used since boot or since the last time the counter rolled over. You + * can get average power over some time window T by sampling before and + * after and doing the following calculation + * + * power_uw = (energy_uj_after - energy_uj_before) / seconds + * + * To get the power in Watts (rather than microwatts), you can simply + * divide by 1000000. + * + * Because we're only concerned with the ratio between the GPU and CPU + * power, we never bother dividing by 1000000 the length of time of the + * sampling window because that would just algebraically cancel out. + * Instead, we divide the GPU energy used in the window (difference of + * before and after) by the CPU energy used. It nicely provides the + * ratio of the averages and there are no instantaneous sampling + * problems. + * + * Overflow is possible here. However, that would simply mean that + * the HW counter has overflowed and us wrapping around is probably + * the right thing to do. Wrapping at 32 bits is exactly what the + * Linux kernel's turbostat utility does so it's probably right. + */ + uint32_t cpu_energy_delta_uj = cpu_energy_uj - self->last_cpu_energy_uj; + uint32_t igpu_energy_delta_uj = igpu_energy_uj - self->last_igpu_energy_uj; + self->last_cpu_energy_uj = cpu_energy_uj; + self->last_igpu_energy_uj = igpu_energy_uj; + + if (cpu_energy_delta_uj == 0) { + LOG_ERROR("CPU reported no energy used\n"); + goto unlock; + } + + float threshold = config_get_igpu_power_threshold(self->config); + double ratio = (double)igpu_energy_delta_uj / (double)cpu_energy_delta_uj; + if (ratio > threshold) { + game_mode_set_governor(self, GAME_MODE_GOVERNOR_IGPU_DESIRED); + } else { + game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED); + } + +unlock: + pthread_rwlock_unlock(&self->rwlock); +} + /** * Pivot into game mode. * @@ -189,30 +349,11 @@ static void game_mode_context_enter(GameModeContext *self) LOG_MSG("Entering Game Mode...\n"); sd_notifyf(0, "STATUS=%sGameMode is now active.%s\n", "\x1B[1;32m", "\x1B[0m"); - /* Read the initial governor state so we can revert it correctly */ - const char *initial_state = get_gov_state(); - if (initial_state) { - /* store the initial cpu governor mode */ - strncpy(self->initial_cpu_mode, initial_state, sizeof(self->initial_cpu_mode) - 1); - self->initial_cpu_mode[sizeof(self->initial_cpu_mode) - 1] = '\0'; - LOG_MSG("governor was initially set to [%s]\n", initial_state); - - /* Choose the desired governor */ - char desired[CONFIG_VALUE_MAX] = { 0 }; - config_get_desired_governor(self->config, desired); - const char *desiredGov = desired[0] != '\0' ? desired : "performance"; - - const char *const exec_args[] = { - "/usr/bin/pkexec", LIBEXECDIR "/cpugovctl", "set", desiredGov, NULL, - }; - - LOG_MSG("Requesting update of governor policy to %s\n", desiredGov); - if (run_external_process(exec_args, NULL, -1) != 0) { - LOG_ERROR("Failed to update cpu governor policy\n"); - /* if the set fails, clear the initial mode so we don't try and reset it back and fail - * again, presumably */ - memset(self->initial_cpu_mode, 0, sizeof(self->initial_cpu_mode)); - } + if (game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED) == 0) { + /* We just switched to a non-default governor. Enable the iGPU + * optimization. + */ + game_mode_enable_igpu_optimization(self); } /* Inhibit the screensaver */ @@ -250,24 +391,9 @@ static void game_mode_context_leave(GameModeContext *self) if (config_get_inhibit_screensaver(self->config)) game_mode_inhibit_screensaver(false); - /* Reset the governer state back to initial */ - if (self->initial_cpu_mode[0] != '\0') { - /* Choose the governor to reset to, using the config to override */ - char defaultgov[CONFIG_VALUE_MAX] = { 0 }; - config_get_default_governor(self->config, defaultgov); - const char *gov_mode = defaultgov[0] != '\0' ? defaultgov : self->initial_cpu_mode; + game_mode_set_governor(self, GAME_MODE_GOVERNOR_DEFAULT); - const char *const exec_args[] = { - "/usr/bin/pkexec", LIBEXECDIR "/cpugovctl", "set", gov_mode, NULL, - }; - - LOG_MSG("Requesting update of governor policy to %s\n", gov_mode); - if (run_external_process(exec_args, NULL, -1) != 0) { - LOG_ERROR("Failed to update cpu governor policy\n"); - } - - memset(self->initial_cpu_mode, 0, sizeof(self->initial_cpu_mode)); - } + game_mode_disable_igpu_optimization(self); char scripts[CONFIG_LIST_MAX][CONFIG_VALUE_MAX]; memset(scripts, 0, sizeof(scripts)); @@ -801,6 +927,9 @@ static void *game_mode_context_reaper(void *userdata) return NULL; } + /* Check on the CPU/iGPU energy balance */ + game_mode_check_igpu_energy(self); + /* Expire remaining entries */ game_mode_context_auto_expire(self); diff --git a/example/gamemode.ini b/example/gamemode.ini index d9e08d0..9c59857 100644 --- a/example/gamemode.ini +++ b/example/gamemode.ini @@ -1,5 +1,5 @@ [general] -; The reaper thread will check every 5 seconds for exited clients and for config file changes +; The reaper thread will check every 5 seconds for exited clients, for config file changes, and for the CPU/iGPU power balance reaper_freq=5 ; The desired governor is used when entering GameMode instead of "performance" @@ -7,6 +7,15 @@ desiredgov=performance ; The default governer is used when leaving GameMode instead of restoring the original value defaultgov=powersave +; 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. +; This is a ratio of iGPU Watts / CPU Watts which is used to determine when the +; integraged GPU is under heavy enough load to justify switching to +; igpu_desiredgov. Set this to -1 to disable all iGPU checking and always +; use desiredgov for games. +igpu_power_threshold=0.3 + ; GameMode can change the scheduler policy to SCHED_ISO on kernels which support it (currently ; not supported by upstream kernels). Can be set to "auto", "on" or "off". "auto" will enable ; with 4 or more CPU cores. "on" will always enable. Defaults to "off".