Prechádzať zdrojové kódy

Merge pull request #101 from mdiluz/gpu-optimisations

Add Preliminary GPU Optimization (overclocking) features
Alex Smith 6 rokov pred
rodič
commit
536be9c4c4

+ 4 - 0
README.md

@@ -8,6 +8,7 @@ Currently GameMode includes support for optimisations including:
 * I/O Priority
 * Kernel Scheduler (`SCHED_ISO`)
 * Screensaver inhibiting
+* GPU Overclocking (Nvidia and AMD)
 * Custom scripts
 
 Issues with GameMode should be reported here in the issues section, and not reported to Feral directly.
@@ -106,6 +107,9 @@ If you are unsure, `bootstrap.sh` will warn you if your system lacks CPU governo
 
 Scripts and other features will still work.
 
+### GPU Optimisations
+GameMode is able to automatically apply GPU overclocks when activated. AMD overclocking currently requires the amdgpu kernel module, and Nvidia requires the `coolbits` extension to be enabled in the Nvidia settings. It is very much encouraged for users to find out their own overclocking limits manually before venturing into configuring them in GameMode, and activating this feature in GameMode assumes you take responsibility for the effects of said overclocks. More information can be found in the `example/gamemode.ini` file. Note that both Nvidia (GPUBoost) and AMD (Overdrive) devices and drivers already attempt to internally overclock if possible, but it is still common for enthusiasts to want to manually push the upper threshold.
+
 ---
 ## Developers
 

+ 105 - 4
daemon/daemon_config.c

@@ -75,6 +75,15 @@ struct GameModeConfig {
 	long inhibit_screensaver;
 
 	long reaper_frequency;
+
+	char apply_gpu_optimisations[CONFIG_VALUE_MAX];
+	long gpu_vendor;
+	long gpu_device;
+	long nv_core_clock_mhz_offset;
+	long nv_mem_clock_mhz_offset;
+	long nv_perf_level;
+	long amd_core_clock_percentage;
+	long amd_mem_clock_percentage;
 };
 
 /*
@@ -111,7 +120,7 @@ static bool append_value_to_list(const char *list_name, const char *value,
 }
 
 /*
- * Get a positive long value from a string
+ * Get a long value from a string
  */
 static bool get_long_value(const char *value_name, const char *value, long *output)
 {
@@ -121,7 +130,27 @@ static bool get_long_value(const char *value_name, const char *value, long *outp
 	if (errno == ERANGE) {
 		LOG_ERROR("Config: %s overflowed, given [%s]\n", value_name, value);
 		return false;
-	} else if (config_value <= 0 || !(*value != '\0' && end && *end == '\0')) {
+	} 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;
+}
+/*
+ * Get a long value from a hex string
+ */
+static bool get_long_value_hex(const char *value_name, const char *value, long *output)
+{
+	char *end = NULL;
+	long config_value = strtol(value, &end, 16);
+
+	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 {
@@ -173,6 +202,25 @@ static int inih_handler(void *user, const char *section, const char *name, const
 		} else if (strcmp(name, "inhibit_screensaver") == 0) {
 			valid = get_long_value(name, value, &self->inhibit_screensaver);
 		}
+	} else if (strcmp(section, "gpu") == 0) {
+		/* GPU subsection */
+		if (strcmp(name, "apply_gpu_optimisations") == 0) {
+			valid = get_string_value(value, self->apply_gpu_optimisations);
+		} else if (strcmp(name, "gpu_vendor") == 0) {
+			valid = get_long_value_hex(name, value, &self->gpu_vendor);
+		} else if (strcmp(name, "gpu_device") == 0) {
+			valid = get_long_value(name, value, &self->gpu_device);
+		} else if (strcmp(name, "nv_core_clock_mhz_offset") == 0) {
+			valid = get_long_value(name, value, &self->nv_core_clock_mhz_offset);
+		} else if (strcmp(name, "nv_mem_clock_mhz_offset") == 0) {
+			valid = get_long_value(name, value, &self->nv_mem_clock_mhz_offset);
+		} else if (strcmp(name, "nv_perf_level") == 0) {
+			valid = get_long_value(name, value, &self->nv_perf_level);
+		} else if (strcmp(name, "amd_core_clock_percentage") == 0) {
+			valid = get_long_value(name, value, &self->amd_core_clock_percentage);
+		} else if (strcmp(name, "amd_mem_clock_percentage") == 0) {
+			valid = get_long_value(name, value, &self->amd_mem_clock_percentage);
+		}
 	} else if (strcmp(section, "custom") == 0) {
 		/* Custom subsection */
 		if (strcmp(name, "start") == 0) {
@@ -231,9 +279,17 @@ static void load_config_files(GameModeConfig *self)
 	memset(self->defaultgov, 0, sizeof(self->defaultgov));
 	memset(self->desiredgov, 0, sizeof(self->desiredgov));
 	memset(self->softrealtime, 0, sizeof(self->softrealtime));
-	self->renice = 4; /* default value of 4 */
-	self->reaper_frequency = DEFAULT_REAPER_FREQ;
+	memset(self->apply_gpu_optimisations, 0, sizeof(self->apply_gpu_optimisations));
 	self->inhibit_screensaver = 1; /* Defaults to on */
+	self->renice = 4;              /* default value of 4 */
+	self->reaper_frequency = DEFAULT_REAPER_FREQ;
+	self->gpu_vendor = 0;
+	self->gpu_device = -1; /* 0 is a valid device ID so use -1 to indicate no value */
+	self->nv_core_clock_mhz_offset = 0;
+	self->nv_mem_clock_mhz_offset = 0;
+	self->nv_perf_level = -1;
+	self->amd_core_clock_percentage = 0;
+	self->amd_mem_clock_percentage = 0;
 
 	/*
 	 * Locations to load, in order
@@ -461,3 +517,48 @@ void config_get_ioprio_value(GameModeConfig *self, int *value)
 	else
 		*value = atoi(ioprio_value);
 }
+
+/*
+ * Get various config info for gpu optimisations
+ */
+void config_get_apply_gpu_optimisations(GameModeConfig *self, char value[CONFIG_VALUE_MAX])
+{
+	memcpy_locked_config(self,
+	                     value,
+	                     &self->apply_gpu_optimisations,
+	                     sizeof(self->apply_gpu_optimisations));
+}
+
+void config_get_gpu_vendor(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->gpu_vendor, sizeof(long));
+}
+
+void config_get_gpu_device(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->gpu_device, sizeof(long));
+}
+
+void config_get_nv_core_clock_mhz_offset(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->nv_core_clock_mhz_offset, sizeof(long));
+}
+
+void config_get_nv_mem_clock_mhz_offset(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->nv_mem_clock_mhz_offset, sizeof(long));
+}
+void config_get_nv_perf_level(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->nv_perf_level, sizeof(long));
+}
+
+void config_get_amd_core_clock_percentage(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->amd_core_clock_percentage, sizeof(long));
+}
+
+void config_get_amd_mem_clock_percentage(GameModeConfig *self, long *value)
+{
+	memcpy_locked_config(self, value, &self->amd_mem_clock_percentage, sizeof(long));
+}

+ 12 - 0
daemon/daemon_config.h

@@ -129,3 +129,15 @@ void config_get_renice_value(GameModeConfig *self, long *value);
  * Get the ioprio value
  */
 void config_get_ioprio_value(GameModeConfig *self, int *value);
+
+/*
+ * Get various config info for gpu optimisations
+ */
+void config_get_apply_gpu_optimisations(GameModeConfig *self, char value[CONFIG_VALUE_MAX]);
+void config_get_gpu_vendor(GameModeConfig *self, long *value);
+void config_get_gpu_device(GameModeConfig *self, long *value);
+void config_get_nv_core_clock_mhz_offset(GameModeConfig *self, long *value);
+void config_get_nv_mem_clock_mhz_offset(GameModeConfig *self, long *value);
+void config_get_nv_perf_level(GameModeConfig *self, long *value);
+void config_get_amd_core_clock_percentage(GameModeConfig *self, long *value);
+void config_get_amd_mem_clock_percentage(GameModeConfig *self, long *value);

+ 0 - 1
daemon/dbus_messaging.c

@@ -34,7 +34,6 @@ POSSIBILITY OF SUCH DAMAGE.
 #include "dbus_messaging.h"
 #include "daemonize.h"
 #include "gamemode.h"
-#include "governors.h"
 #include "logging.h"
 
 #include <stdlib.h>

+ 7 - 16
daemon/governors.c → daemon/external-helper.c

@@ -31,9 +31,6 @@ POSSIBILITY OF SUCH DAMAGE.
 
 #define _GNU_SOURCE
 
-#include "governors.h"
-#include "config.h"
-#include "governors-query.h"
 #include "logging.h"
 
 #include <linux/limits.h>
@@ -42,21 +39,15 @@ POSSIBILITY OF SUCH DAMAGE.
 #include <unistd.h>
 
 /**
- * Update the governors to the given argument, via pkexec
+ * Call an external process
  */
-bool set_governors(const char *value)
+int run_external_process(const char *const *exec_args)
 {
 	pid_t p;
 	int status = 0;
 	int ret = 0;
 	int r = -1;
 
-	const char *const exec_args[] = {
-		"/usr/bin/pkexec", LIBEXECDIR "/cpugovctl", "set", value, NULL,
-	};
-
-	LOG_MSG("Requesting update of governor policy to %s\n", value);
-
 	if ((p = fork()) < 0) {
 		LOG_ERROR("Failed to fork(): %s\n", strerror(errno));
 		return false;
@@ -69,14 +60,14 @@ bool set_governors(const char *value)
 		 * http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html
 		 */
 		if ((r = execv(exec_args[0], (char *const *)exec_args)) != 0) {
-			LOG_ERROR("Failed to execute cpugovctl helper: %s %s\n", exec_args[1], strerror(errno));
+			LOG_ERROR("Failed to execute external process: %s %s\n", exec_args[0], strerror(errno));
 			exit(EXIT_FAILURE);
 		}
 		_exit(EXIT_SUCCESS);
 	} else {
 		if (waitpid(p, &status, 0) < 0) {
 			LOG_ERROR("Failed to waitpid(%d): %s\n", (int)p, strerror(errno));
-			return false;
+			return -1;
 		}
 		/* i.e. sigsev */
 		if (!WIFEXITED(status)) {
@@ -85,9 +76,9 @@ bool set_governors(const char *value)
 	}
 
 	if ((ret = WEXITSTATUS(status)) != 0) {
-		LOG_ERROR("Failed to update cpu governor policy\n");
-		return false;
+		LOG_ERROR("External process failed\n");
+		return -1;
 	}
 
-	return true;
+	return 0;
 }

+ 2 - 7
daemon/governors.h → daemon/external-helper.h

@@ -31,10 +31,5 @@ POSSIBILITY OF SUCH DAMAGE.
 
 #pragma once
 
-#include <stdbool.h>
-
-/**
- * Update all governors to the given value. If this is NULL, restore the
- * initial governor.
- */
-bool set_governors(const char *value);
+/* Run an external process and capture the return value */
+int run_external_process(const char *const *exec_args);

+ 222 - 0
daemon/gamemode-gpu.c

@@ -0,0 +1,222 @@
+
+/*
+
+Copyright (c) 2017-2018, 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.
+
+ */
+
+#define _GNU_SOURCE
+
+#include "config.h"
+#include "external-helper.h"
+#include "helpers.h"
+#include "logging.h"
+
+#include "gamemode.h"
+
+#include "daemon_config.h"
+#include "gpu-control.h"
+
+/**
+ * Attempts to identify the current in use GPU information
+ */
+int game_mode_initialise_gpu(GameModeConfig *config, GameModeGPUInfo **info)
+{
+	int status = 0;
+
+	/* Verify input, this is programmer error */
+	if (!info || *info)
+		FATAL_ERROR("Invalid GameModeGPUInfo passed to %s", __func__);
+
+	/* Early out if we have this feature turned off */
+	char apply[CONFIG_VALUE_MAX];
+	config_get_apply_gpu_optimisations(config, apply);
+	if (strlen(apply) == 0) {
+		return 0;
+	} else if (strncmp(apply, "accept-responsibility", CONFIG_VALUE_MAX) != 0) {
+		LOG_ERROR(
+		    "apply_gpu_optimisations set to value other than \"accept-responsibility\" (%s), will "
+		    "not apply GPU optimisations!\n",
+		    apply);
+		return -1;
+	}
+
+	/* Create the context */
+	GameModeGPUInfo *new_info = malloc(sizeof(GameModeGPUInfo));
+	memset(new_info, 0, sizeof(GameModeGPUInfo));
+
+	/* Get the config parameters */
+	config_get_gpu_vendor(config, &new_info->vendor);
+	config_get_gpu_device(config, &new_info->device);
+
+	/* verify device ID */
+	if (new_info->device == -1) {
+		LOG_ERROR(
+		    "Invalid gpu_device value set in configuration, will not apply "
+		    "optimisations!\n");
+		free(new_info);
+		return -1;
+	}
+
+	/* verify GPU vendor */
+	if (!GPUVendorValid(new_info->vendor)) {
+		LOG_ERROR(
+		    "Invalid gpu_vendor value (0x%04x) set in configuration, will not apply "
+		    "optimisations!\n",
+		    (unsigned int)new_info->vendor);
+		LOG_ERROR("Possible values are: 0x%04x (NVIDIA) 0x%04x (AMD) 0x%04x (Intel)\n",
+		          Vendor_NVIDIA,
+		          Vendor_AMD,
+		          Vendor_Intel);
+		free(new_info);
+		return -1;
+	}
+
+	/* Load the config based on GPU and also verify the values are sane */
+	switch (new_info->vendor) {
+	case Vendor_NVIDIA:
+		config_get_nv_core_clock_mhz_offset(config, &new_info->core);
+		config_get_nv_mem_clock_mhz_offset(config, &new_info->mem);
+
+		/* Reject values over some guessed values
+		 * If a user wants to go into very unsafe levels they can recompile
+		 */
+		const int nv_core_hard_limit = 200;
+		const int nv_mem_hard_limit = 2000;
+		if (new_info->core > nv_core_hard_limit || new_info->mem > nv_mem_hard_limit) {
+			LOG_ERROR(
+			    "NVIDIA Overclock value above safety levels of +%d (core) +%d (mem), will "
+			    "not overclock!\n",
+			    nv_core_hard_limit,
+			    nv_mem_hard_limit);
+			LOG_ERROR("nv_core_clock_mhz_offset:%ld nv_mem_clock_mhz_offset:%ld\n",
+			          new_info->core,
+			          new_info->mem);
+			free(new_info);
+			return -1;
+		}
+
+		/* Sanity check the performance level value as well */
+		config_get_nv_perf_level(config, &new_info->nv_perf_level);
+		if (new_info->nv_perf_level < 0 || new_info->nv_perf_level > 16) {
+			LOG_ERROR(
+			    "NVIDIA Performance level value likely invalid (%ld), will not apply "
+			    "optimisations!\n",
+			    new_info->nv_perf_level);
+			free(new_info);
+			return -1;
+		}
+
+		break;
+	case Vendor_AMD:
+		config_get_amd_core_clock_percentage(config, &new_info->core);
+		config_get_amd_mem_clock_percentage(config, &new_info->mem);
+
+		/* Reject values over 20%
+		 * If a user wants to go into very unsafe levels they can recompile
+		 * As far as I can tell the driver doesn't allow values over 20 anyway
+		 */
+		const int amd_hard_limit = 20;
+		if (new_info->core > amd_hard_limit || new_info->mem > amd_hard_limit) {
+			LOG_ERROR("AMD Overclock value above safety level of %d%%, will not overclock!\n",
+			          amd_hard_limit);
+			LOG_ERROR("amd_core_clock_percentage:%ld amd_mem_clock_percentage:%ld\n",
+			          new_info->core,
+			          new_info->mem);
+			free(new_info);
+			return -1;
+		}
+		break;
+	default:
+		break;
+	}
+
+	/* Give back the new gpu info */
+	*info = new_info;
+	return status;
+}
+
+/* Simply used to free the GPU info object */
+void game_mode_free_gpu(GameModeGPUInfo **info)
+{
+	/* Simply free the object */
+	free(*info);
+	*info = NULL;
+}
+
+//#include <linux/limits.h>
+//#include <stdio.h>
+//#include <sys/wait.h>
+//#include <unistd.h>
+
+/**
+ * Applies GPU optimisations when gamemode is active and removes them after
+ */
+int game_mode_apply_gpu(const GameModeGPUInfo *info, bool apply)
+{
+	// Null info means don't apply anything
+	if (!info)
+		return 0;
+
+	LOG_MSG("Requesting GPU optimisations on device:%ld with settings core:%ld clock:%ld\n",
+	        info->device,
+	        info->core,
+	        info->mem);
+
+	/* Generate the input strings */
+	char vendor[7];
+	snprintf(vendor, 7, "0x%04x", (short)info->vendor);
+	char device[4];
+	snprintf(device, 4, "%ld", info->device);
+	char core[8];
+	snprintf(core, 8, "%ld", info->core);
+	char mem[8];
+	snprintf(mem, 8, "%ld", info->mem);
+	char nv_perf_level[4];
+	snprintf(nv_perf_level, 4, "%ld", info->nv_perf_level);
+
+	// Set up our command line to pass to gpuclockctl
+	const char *const exec_args[] = {
+		"/usr/bin/pkexec",
+		LIBEXECDIR "/gpuclockctl",
+		vendor,
+		device,
+		"set",
+		apply ? core : "0",
+		apply ? mem : "0",
+		info->vendor == Vendor_NVIDIA ? nv_perf_level : NULL, /* Only use this if Nvidia */
+		NULL,
+	};
+
+	if (run_external_process(exec_args) != 0) {
+		LOG_ERROR("Failed to call gpuclockctl, could not apply optimisations!\n");
+		return -1;
+	}
+
+	return 0;
+}

+ 32 - 4
daemon/gamemode.c

@@ -32,10 +32,11 @@ POSSIBILITY OF SUCH DAMAGE.
 #define _GNU_SOURCE
 
 #include "gamemode.h"
+#include "config.h"
 #include "daemon_config.h"
 #include "dbus_messaging.h"
+#include "external-helper.h"
 #include "governors-query.h"
-#include "governors.h"
 #include "helpers.h"
 #include "logging.h"
 
@@ -63,6 +64,8 @@ struct GameModeContext {
 
 	char initial_cpu_mode[64]; /**<Only updates when we can */
 
+	struct GameModeGPUInfo *gpu_info; /**<Stored GPU info for the current GPU */
+
 	/* Reaper control */
 	struct {
 		pthread_t thread;
@@ -107,6 +110,9 @@ void game_mode_context_init(GameModeContext *self)
 	self->config = config_create();
 	config_init(self->config);
 
+	/* Initialise the current GPU info */
+	game_mode_initialise_gpu(self->config, &self->gpu_info);
+
 	pthread_rwlock_init(&self->rwlock, NULL);
 	pthread_mutex_init(&self->reaper.mutex, NULL);
 	pthread_cond_init(&self->reaper.condition, NULL);
@@ -144,6 +150,9 @@ void game_mode_context_destroy(GameModeContext *self)
 	pthread_cond_destroy(&self->reaper.condition);
 	pthread_mutex_destroy(&self->reaper.mutex);
 
+	/* Destroy the gpu object */
+	game_mode_free_gpu(&self->gpu_info);
+
 	/* Destroy the config object */
 	config_destroy(self->config);
 
@@ -189,8 +198,13 @@ static void game_mode_context_enter(GameModeContext *self)
 		config_get_desired_governor(self->config, desired);
 		const char *desiredGov = desired[0] != '\0' ? desired : "performance";
 
-		/* set the governor to performance */
-		if (!set_governors(desiredGov)) {
+		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) != 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));
@@ -200,6 +214,9 @@ static void game_mode_context_enter(GameModeContext *self)
 	/* Inhibit the screensaver */
 	if (config_get_inhibit_screensaver(self->config))
 		game_mode_inhibit_screensaver(true);
+
+	/* Apply GPU optimisations */
+	game_mode_apply_gpu(self->gpu_info, true);
 }
 
 /**
@@ -213,6 +230,9 @@ static void game_mode_context_leave(GameModeContext *self)
 	LOG_MSG("Leaving Game Mode...\n");
 	sd_notifyf(0, "STATUS=%sGameMode is currently deactivated.%s\n", "\x1B[1;36m", "\x1B[0m");
 
+	/* Remove GPU optimisations */
+	game_mode_apply_gpu(self->gpu_info, false);
+
 	/* UnInhibit the screensaver */
 	if (config_get_inhibit_screensaver(self->config))
 		game_mode_inhibit_screensaver(false);
@@ -224,7 +244,15 @@ static void game_mode_context_leave(GameModeContext *self)
 		config_get_default_governor(self->config, defaultgov);
 		const char *gov_mode = defaultgov[0] != '\0' ? defaultgov : self->initial_cpu_mode;
 
-		set_governors(gov_mode);
+		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) != 0) {
+			LOG_ERROR("Failed to update cpu governor policy\n");
+		}
+
 		memset(self->initial_cpu_mode, 0, sizeof(self->initial_cpu_mode));
 	}
 

+ 8 - 0
daemon/gamemode.h

@@ -136,3 +136,11 @@ char *game_mode_resolve_wine_preloader(const pid_t pid);
  * Provides a test suite to verify gamemode behaviour
  */
 int game_mode_run_client_tests(void);
+
+/** gamemode-gpu.c
+ * Provides internal APU functions to apply optimisations to gpus
+ */
+typedef struct GameModeGPUInfo GameModeGPUInfo;
+int game_mode_initialise_gpu(GameModeConfig *config, GameModeGPUInfo **info);
+void game_mode_free_gpu(GameModeGPUInfo **info);
+int game_mode_apply_gpu(const GameModeGPUInfo *info, bool apply);

+ 54 - 0
daemon/gpu-control.h

@@ -0,0 +1,54 @@
+/*
+
+Copyright (c) 2017-2018, 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
+
+/* Enums for GPU vendors */
+enum GPUVendor {
+	Vendor_Invalid = 0,
+	Vendor_NVIDIA = 0x10de,
+	Vendor_AMD = 0x1002,
+	Vendor_Intel = 0x8086
+};
+
+#define GPUVendorValid(vendor)                                                                     \
+	(vendor == Vendor_NVIDIA || vendor == Vendor_AMD || vendor == Vendor_Intel)
+
+/* Storage for GPU info*/
+struct GameModeGPUInfo {
+	long vendor;
+	long device; /* path to device, ie. /sys/class/drm/card#/ */
+
+	long core; /* Core clock to apply */
+	long mem;  /* Mem clock to apply */
+
+	long nv_perf_level; /* The Nvidia Performance Level to adjust */
+};

+ 246 - 0
daemon/gpuclockctl.c

@@ -0,0 +1,246 @@
+/*
+
+Copyright (c) 2017-2018, 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.
+
+ */
+
+#define _GNU_SOURCE
+
+#include "logging.h"
+
+#include "external-helper.h"
+#include "gpu-control.h"
+
+/* Plausible extras to add:
+ * Intel support - https://blog.ffwll.ch/2013/03/overclocking-your-intel-gpu-on-linux.html
+ * AMD - Allow setting fan speed as well
+ * Store baseline values with get_gpu_state to apply when leaving gamemode
+ */
+
+/* Helper to quit with usage */
+static const char *usage_text =
+    "usage: gpuclockctl PCI_ID DEVICE [get] [set CORE MEM [PERF_LEVEL]]]";
+static void print_usage_and_exit(void)
+{
+	fprintf(stderr, "%s\n", usage_text);
+	exit(EXIT_FAILURE);
+}
+
+/**
+ * Get the gpu state
+ * Populates the struct with the GPU info on the system
+ */
+int get_gpu_state(struct GameModeGPUInfo *info)
+{
+	fprintf(stderr, "Fetching GPU state is currently unimplemented!\n");
+	return info != NULL;
+}
+
+/**
+ * Set the gpu state based on input parameters on Nvidia
+ */
+int set_gpu_state_nv(struct GameModeGPUInfo *info)
+{
+	if (info->vendor != Vendor_NVIDIA)
+		return -1;
+
+	// These commands don't technically even need root
+
+	/* Set the GPUGraphicsClockOffset parameter */
+	char core_arg[128];
+	snprintf(core_arg,
+	         128,
+	         "[gpu:%ld]/GPUGraphicsClockOffset[%ld]=%ld",
+	         info->device,
+	         info->nv_perf_level,
+	         info->core);
+	const char *exec_args_core[] = { "/usr/bin/nvidia-settings", "-a", core_arg, NULL };
+	if (run_external_process(exec_args_core) != 0) {
+		LOG_ERROR("Failed to set %s!\n", core_arg);
+		return -1;
+	}
+
+	/* Set the GPUMemoryTransferRateOffset parameter */
+	char mem_arg[128];
+	snprintf(mem_arg,
+	         128,
+	         "[gpu:%ld]/GPUMemoryTransferRateOffset[%ld]=%ld",
+	         info->device,
+	         info->nv_perf_level,
+	         info->mem);
+	const char *exec_args_mem[] = { "/usr/bin/nvidia-settings", "-a", mem_arg, NULL };
+	if (run_external_process(exec_args_mem) != 0) {
+		LOG_ERROR("Failed to set %s!\n", mem_arg);
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ * Sets the value in a file in the AMDGPU driver config
+ * Files are:
+ * /sys/class/drm/card0/device/pp_sclk_od
+ * /sys/class/drm/card0/device/pp_mclk_od
+ */
+static int set_gpu_state_amd_file(const char *filename, long device, long value)
+{
+	const char *drm_path = "/sys/class/drm/card%ld/device/%s";
+	char path[64];
+	snprintf(path, 64, drm_path, device, filename);
+
+	FILE *file = fopen(path, "w");
+	if (!file) {
+		LOG_ERROR("Could not open %s for write (%s)!\n", path, strerror(errno));
+		return -1;
+	}
+
+	if (fprintf(file, "%ld", value) < 0) {
+		LOG_ERROR("Could not write to %s (%s)!\n", path, strerror(errno));
+		return -1;
+	}
+
+	if (fclose(file) != 0) {
+		LOG_ERROR("Could not close %s after writing (%s)!\n", path, strerror(errno));
+		return -1;
+	}
+
+	return 0;
+}
+
+/**
+ * Set the gpu state based on input parameters on amd
+ */
+int set_gpu_state_amd(struct GameModeGPUInfo *info)
+{
+	if (info->vendor != Vendor_AMD)
+		return -1;
+
+	// Set the the core and mem clock speeds using the OverDrive files
+	if (set_gpu_state_amd_file("pp_sclk_od", info->device, info->core) != 0)
+		return -1;
+	if (set_gpu_state_amd_file("pp_mclk_od", info->device, info->mem) != 0)
+		return -1;
+
+	return 0;
+}
+
+/* Helper to get and verify vendor value */
+static long get_vendor(const char *val)
+{
+	char *end;
+	long ret = strtol(val, &end, 0);
+	if (!GPUVendorValid(ret) || end == val) {
+		LOG_ERROR("Invalid GPU Vendor passed (0x%04x)!\n", (unsigned short)ret);
+		print_usage_and_exit();
+	}
+	return ret;
+}
+
+/* Helper to get and verify device value */
+static long get_device(const char *val)
+{
+	char *end;
+	long ret = strtol(val, &end, 10);
+	if (ret < 0 || end == val) {
+		LOG_ERROR("Invalid GPU device passed (%ld)!\n", ret);
+		print_usage_and_exit();
+	}
+	return ret;
+}
+
+/* Helper to get and verify core and mem value */
+static long get_generic_value(const char *val)
+{
+	char *end;
+	long ret = strtol(val, &end, 10);
+	if (ret < 0 || end == val) {
+		LOG_ERROR("Invalid value passed (%ld)!\n", ret);
+		print_usage_and_exit();
+	}
+	return ret;
+}
+
+/**
+ * Main entry point, dispatch to the appropriate helper
+ */
+int main(int argc, char *argv[])
+{
+	if (argc == 4 && strncmp(argv[3], "get", 3) == 0) {
+		/* Get and verify the vendor and device */
+		struct GameModeGPUInfo info;
+		memset(&info, 0, sizeof(info));
+		info.vendor = get_vendor(argv[1]);
+		info.device = get_device(argv[2]);
+
+		/* Fetch the state and print it out */
+		get_gpu_state(&info);
+		printf("%ld %ld\n", info.core, info.mem);
+
+	} else if (argc >= 6 && argc <= 7 && strncmp(argv[3], "set", 3) == 0) {
+		/* Must be root to set the state */
+		if (geteuid() != 0) {
+			fprintf(stderr, "gpuclockctl must be run as root to set values\n");
+			print_usage_and_exit();
+		}
+
+		/* Get and verify the vendor and device */
+		struct GameModeGPUInfo info;
+		memset(&info, 0, sizeof(info));
+		info.vendor = get_vendor(argv[1]);
+		info.device = get_device(argv[2]);
+		info.core = get_generic_value(argv[4]);
+		info.mem = get_generic_value(argv[5]);
+
+		if (info.vendor == Vendor_NVIDIA)
+			info.nv_perf_level = get_generic_value(argv[6]);
+
+		printf("gpuclockctl setting core:%ld mem:%ld on device:%ld with vendor 0x%04x\n",
+		       info.core,
+		       info.mem,
+		       info.device,
+		       (unsigned short)info.vendor);
+
+		if (info.vendor == Vendor_NVIDIA)
+			printf("on Performance Level %ld\n", info.nv_perf_level);
+
+		switch (info.vendor) {
+		case Vendor_NVIDIA:
+			return set_gpu_state_nv(&info);
+		case Vendor_AMD:
+			return set_gpu_state_amd(&info);
+		default:
+			printf("Currently unsupported GPU vendor 0x%04x, doing nothing!\n", (short)info.vendor);
+			break;
+		}
+	} else {
+		print_usage_and_exit();
+	}
+
+	return EXIT_SUCCESS;
+}

+ 17 - 1
daemon/meson.build

@@ -2,6 +2,7 @@
 common_sources = [
     'logging.c',
     'governors-query.c',
+    'external-helper.c',
 ]
 
 daemon_common = static_library(
@@ -24,9 +25,9 @@ daemon_sources = [
     'gamemode-sched.c',
     'gamemode-wine.c',
     'gamemode-tests.c',
+    'gamemode-gpu.c',
     'daemonize.c',
     'dbus_messaging.c',
-    'governors.c',
     'daemon_config.c',
 ]
 
@@ -61,3 +62,18 @@ cpugovctl = executable(
     install: true,
     install_dir: path_libexecdir,
 )
+
+# Small target util to get and set gpu clocks values
+gpuclockctl_sources = [
+    'gpuclockctl.c',
+]
+
+gpuclockctl = executable(
+    'gpuclockctl',
+    sources: gpuclockctl_sources,
+    dependencies: [
+        link_daemon_common,
+    ],
+    install: true,
+    install_dir: path_libexecdir,
+)

+ 12 - 0
data/com.feralinteractive.GameMode.policy.in

@@ -23,4 +23,16 @@
     <annotate key="org.freedesktop.policykit.exec.path">@LIBEXECDIR@/cpugovctl</annotate>
   </action>
 
+  <action id="com.feralinteractive.GameMode.gpu-helper">
+    <description>Modify the GPU clock states</description>
+    <message>Authentication is required to modify the GPU clock states</message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>yes</allow_active>
+    </defaults>
+    <annotate key="org.freedesktop.policykit.exec.path">@LIBEXECDIR@/gpuclockctl</annotate>
+    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
+  </action>
+
 </policyconfig>

+ 27 - 0
example/gamemode.ini

@@ -34,6 +34,33 @@ inhibit_screensaver=1
 ;blacklist=HalfLife3
 ;    glxgears
 
+[gpu]
+; Here Be Dragons!
+; Warning: Use these settings at your own risk
+; Any damage to hardware incurred due to this feature is your responsibility and yours alone
+; It is also highly recommended you try these settings out first manually to find the sweet spots
+
+; Setting this to the keyphrase "accept-responsibility" will allow gamemode to apply GPU optimisations such as overclocks
+;apply_gpu_optimisations=0
+
+; You must set these to tell gamemode the Vendor of your graphics card, as well it's device number on the system (usually 0)
+; Vendor must be one of 0x10de (NVIDIA), 0x1002 (AMD) or 0x8086 (Intel)
+;gpu_vendor=0x0000
+;gpu_device=0
+
+; Nvidia specific settings (these are Mhz offsets from the baseline, ie. 0 applies no change)
+; Requires the coolbits extension activated in nvidia-xconfig
+;nv_core_clock_mhz_offset=0
+;nv_mem_clock_mhz_offset=0
+; This corresponds to the performance level to edit in nvidia-xconfig
+;nv_perf_level=1
+
+; AMD specific settings (these are percentages applied on top of the baseline, ie. 0 applies no change)
+; Requires the the AMDGPU kernel module
+; It is also highly recommended you use lm-sensors (or other available tools) to verify card temperatures
+;amd_core_clock_percentage=0
+;amd_mem_clock_percentage=0
+
 [custom]
 ; Custom scripts (executed using the shell) when gamemode starts and ends
 ;start=notify-send "GameMode started"