From 688373a2607c2b365a79a14ccf27cab46b69cf6f Mon Sep 17 00:00:00 2001 From: Jason Ekstrand Date: Mon, 16 Dec 2019 12:32:01 -0600 Subject: [PATCH] Add an option for using a different governor for integrated GPUs This commit adds two new configuration options: igpu_desiredgov and igpu_power_threshold which allow for a different CPU governor when the Intel integrated GPU is under load. This currently only applies to Intel integrated GPUs and not AMD APUs because it uses the Intel RAPL infrastructure for getting power information. If on a platform that without an Intel integrated GPU or where the kernel does not support RAPL, the new options will be ignored and it will fall back to the old behavior. One of the core principals of gamemoded to date has been that, when playing a game, we want to use the "performance" CPU governor to increase CPU performance and prevent CPU-limiting. However, when the integrated GPU is under load, this can be counter-productive because the CPU and GPU share a thermal and power budget. By throwing the CPU governor to "performance" game mode currently makes the CPU frequency management far too aggressive and it burns more power than needed. With a discrete GPU, this is fine because the worst that happens is a bit more fan noise. With an integrated GPU, however, the additional power being burned by the CPU is power not available to the GPU and this can cause the GPU to clock down and lead to significantly worse performance. By using the "powersave" governor instead of the "performance" governor while the integrated GPU is under load, we can save power on the CPU side which lets the GPU clock up higher. On my Razer Blade Stealth 13 with an i7-1065G7, this improves the performance of "Shadow of the Tomb Raider" by around 25-30% according to its internal benchmark mode. --- daemon/gamemode-config.c | 61 +++++++++++++++++++++ daemon/gamemode-config.h | 2 + daemon/gamemode-context.c | 110 +++++++++++++++++++++++++++++++++++++- example/gamemode.ini | 11 +++- 4 files changed, 182 insertions(+), 2 deletions(-) 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 1f3c894..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" @@ -66,6 +67,7 @@ struct GameModeClient { enum GameModeGovernor { GAME_MODE_GOVERNOR_DEFAULT, GAME_MODE_GOVERNOR_DESIRED, + GAME_MODE_GOVERNOR_IGPU_DESIRED, }; struct GameModeContext { @@ -82,6 +84,10 @@ struct GameModeContext { struct GameModeGPUInfo *stored_gpu; /**config, gov_config_str); + gov_str = gov_config_str[0] != '\0' ? gov_config_str : "powersave"; + break; + default: assert(!"Invalid governor requested"); } @@ -240,6 +251,93 @@ static int game_mode_set_governor(GameModeContext *self, enum GameModeGovernor g 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. * @@ -251,7 +349,12 @@ 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"); - game_mode_set_governor(self, GAME_MODE_GOVERNOR_DESIRED); + 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 */ if (config_get_inhibit_screensaver(self->config)) @@ -290,6 +393,8 @@ static void game_mode_context_leave(GameModeContext *self) game_mode_set_governor(self, GAME_MODE_GOVERNOR_DEFAULT); + game_mode_disable_igpu_optimization(self); + char scripts[CONFIG_LIST_MAX][CONFIG_VALUE_MAX]; memset(scripts, 0, sizeof(scripts)); config_get_gamemode_end_scripts(self->config, scripts); @@ -822,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".