From 6a7ff4ae31c746ac301b1e71e5c85778b24d243b Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Fri, 9 Feb 2024 07:37:51 -0600 Subject: [PATCH] Add a plugin for amdgpu tuning of the `panel_power_savings` attribute This attribute accepts a range from 0 through 4 where larger values will also have larger panel power savings. Using this has a trade off for color accuracy, and it is only applied when the system is currently operating on battery. The plugin uses upower to get a signal when battery changes so that it will react instantly. Intentionally the plugin will check what values are already programmed to the sysfs file to avoid unnecessary writes. Writing the sysfs file will cause a modeset which isn't necessary if writing the same value twice. The default values are applied to the profiles that are used in power-profiles-daemon compatbility. They also match the values used in that software. Signed-off-by: Mario Limonciello --- profiles/accelerator-performance/tuned.conf | 3 + profiles/balanced/tuned.conf | 3 + profiles/latency-performance/tuned.conf | 3 + profiles/powersave/tuned.conf | 3 + profiles/throughput-performance/tuned.conf | 3 + tuned/plugins/plugin_amdgpu.py | 120 ++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 tuned/plugins/plugin_amdgpu.py diff --git a/profiles/accelerator-performance/tuned.conf b/profiles/accelerator-performance/tuned.conf index 696a1ddd5..339940212 100644 --- a/profiles/accelerator-performance/tuned.conf +++ b/profiles/accelerator-performance/tuned.conf @@ -62,3 +62,6 @@ sched_min_granularity_ns = 10000000 # and reduces their over-scheduling. Synchronous workloads will still # have immediate wakeup/sleep latencies. sched_wakeup_granularity_ns = 15000000 + +[amdgpu] +panel_power_savings=0 diff --git a/profiles/balanced/tuned.conf b/profiles/balanced/tuned.conf index 1dda2f32e..c8410c943 100644 --- a/profiles/balanced/tuned.conf +++ b/profiles/balanced/tuned.conf @@ -29,3 +29,6 @@ radeon_powersave=dpm-balanced, auto [scsi_host] alpm=medium_power + +[amdgpu] +panel_power_savings=3 diff --git a/profiles/latency-performance/tuned.conf b/profiles/latency-performance/tuned.conf index 1dec6905c..a3b8be29e 100644 --- a/profiles/latency-performance/tuned.conf +++ b/profiles/latency-performance/tuned.conf @@ -35,3 +35,6 @@ vm.dirty_background_ratio=3 # 100 tells the kernel to aggressively swap processes out of physical memory # and move them to swap cache vm.swappiness=10 + +[amdgpu] +panel_power_savings=0 diff --git a/profiles/powersave/tuned.conf b/profiles/powersave/tuned.conf index c593a800e..85d7dbbef 100644 --- a/profiles/powersave/tuned.conf +++ b/profiles/powersave/tuned.conf @@ -41,3 +41,6 @@ kernel.nmi_watchdog=0 [script] script=${i:PROFILE_DIR}/script.sh + +[amdgpu] +panel_power_savings=4 diff --git a/profiles/throughput-performance/tuned.conf b/profiles/throughput-performance/tuned.conf index e4e832fa6..c83d3b6d6 100644 --- a/profiles/throughput-performance/tuned.conf +++ b/profiles/throughput-performance/tuned.conf @@ -73,3 +73,6 @@ type=sysctl uname_regex=aarch64 cpuinfo_regex=${thunderx_cpuinfo_regex} kernel.numa_balancing=0 + +[amdgpu] +panel_power_savings=0 diff --git a/tuned/plugins/plugin_amdgpu.py b/tuned/plugins/plugin_amdgpu.py new file mode 100644 index 000000000..82176d070 --- /dev/null +++ b/tuned/plugins/plugin_amdgpu.py @@ -0,0 +1,120 @@ +from . import base +from .decorators import * +import errno +import tuned.logs +import dbus +from tuned.utils.commands import commands + +log = tuned.logs.get() +cmd = commands() + +UPOWER_DBUS_NAME = "org.freedesktop.UPower" +UPOWER_DBUS_PATH = "/org/freedesktop/UPower" +UPOWER_DBUS_INTERFACE = "org.freedesktop.UPower" + + +class AmdgpuPlugin(base.Plugin): + """ + `amdgpu`:: + + Configures panel power savings on laptops with amdgpu driven eDP panels. + This accepts a value range from 0 to 4, where 4 is the highest power savings + but will trade off color accuracy. + + Settings will only be applied when the system is on battery. + """ + + def __init__(self, *args, **kwargs): + super(AmdgpuPlugin, self).__init__(*args, **kwargs) + self.proxy = None + + def upower_changed(self, interface, changed, invalidated): + properties = dbus.Interface(self.proxy, "org.freedesktop.DBus.Properties") + self._on_battery = bool(properties.Get(UPOWER_DBUS_INTERFACE, "OnBattery")) + log.debug(f"🔋: {self._on_battery}, 🎯: {self.target_value}") + for device in self._assigned_devices: + self.apply_target(device) + + def setup_battery_signaling(self): + bus = dbus.SystemBus() + self.proxy = bus.get_object(UPOWER_DBUS_NAME, UPOWER_DBUS_PATH) + self.proxy.connect_to_signal("PropertiesChanged", self.upower_changed) + self.upower_changed(None, None, None) + + def _init_devices(self): + self._assigned_devices = set() + self._free_devices = set() + + for device in ( + self._hardware_inventory.get_devices("drm") + .match_sys_name("card*eDP*") + .match_sys_name("amdgpu") + .match_sys_name("panel_power_savings") + ): + self._free_devices.add(device.sys_name) + self._devices_supported = len(self._free_devices) > 0 + + def _get_device_objects(self, devices): + return [self._hardware_inventory.get_device("drm", x) for x in devices] + + @classmethod + def _get_config_options(cls): + return {"panel_power_savings": None} + + def _instance_init(self, instance): + instance._has_static_tuning = True + instance._has_dynamic_tuning = False + + def _amdgpu_files(self, device): + return { + "panel_power_savings": f"/sys/class/drm/{device}/amdgpu/panel_power_savings" + } + + def _instance_cleanup(self, instance): + pass + + def apply_target(self, device, sim=False): + """Apply the target value to the panel_power_savings file if it doesn't already have it""" + + # if we don't have a proxy, we can't tell if we're on battery + if not self.proxy: + self.setup_battery_signaling() + return None + + # determine value to use (only apply if on battery, otherwise set to 0) + if self._on_battery: + target = self.target_value + else: + target = 0 + + # make sure the value is different (avoids unnecessary kernel modeset) + current = int(self._get_panel_power_savings(device)) + if current == target: + log.info(f"panel_power_savings already {target} [🔋: {self._on_battery}]") + return target + + # flush it out + log.info(f"panel_power_savings -> {target} [🔋: {self._on_battery}]") + if sim or cmd.write_to_file(self._amdgpu_files(device)["panel_power_savings"], target): + return target + return None + + @command_set("panel_power_savings", per_device=True) + def _set_panel_power_savings(self, value, device, sim, remove): + """Set the panel_power_savings value""" + try: + value = int(value, 10) + except ValueError: + log.warn(f"Invalid value {value} for panel_power_savings") + return None + if value in range(0, 5): + self.target_value = value + return self.apply_target(device, sim) + log.warn(f"Invalid value {value} for panel_power_savings") + return None + + @command_get("panel_power_savings") + def _get_panel_power_savings(self, device, ignore_missing=False): + """Get the current panel_power_savings value""" + fname = self._amdgpu_files(device)["panel_power_savings"] + return cmd.read_file(fname, no_error=ignore_missing).strip()