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".