diff --git a/docs/meson.build b/docs/meson.build
index 01e8909..ead4574 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -21,6 +21,7 @@ content_files += gnome.gdbus_codegen(
private_headers = [
'power-profiles-daemon.h',
'ppd-action-trickle-charge.h',
+ 'ppd-action-amdgpu-panel-power.h',
'ppd-driver-fake.h',
'ppd-driver-intel-pstate.h',
'ppd-driver-amd-pstate.h',
diff --git a/docs/power-profiles-daemon-docs.xml b/docs/power-profiles-daemon-docs.xml
index c3c7119..e903706 100644
--- a/docs/power-profiles-daemon-docs.xml
+++ b/docs/power-profiles-daemon-docs.xml
@@ -73,6 +73,7 @@
+
diff --git a/docs/power-profiles-daemon-sections.txt b/docs/power-profiles-daemon-sections.txt
index 9aa5feb..3c88528 100644
--- a/docs/power-profiles-daemon-sections.txt
+++ b/docs/power-profiles-daemon-sections.txt
@@ -14,6 +14,15 @@ PpdAction
PPD_TYPE_ACTION
+
+ppd-action-amdgpu-panel-power
+AMDGPU Power Panel Saving Action
+PpdActionAmdgpuPanelPowerClass
+_PpdActionAmdgpuPanelPower
+
+PPD_TYPE_ACTION
+
+
ppd-driver
Profile Drivers
diff --git a/src/meson.build b/src/meson.build
index 870beba..ee17d8f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -83,6 +83,7 @@ libpower_profiles_daemon_dep = declare_dependency(
sources += [
'power-profiles-daemon.c',
'ppd-action-trickle-charge.c',
+ 'ppd-action-amdgpu-panel-power.c',
'ppd-driver-intel-pstate.c',
'ppd-driver-amd-pstate.c',
'ppd-driver-platform-profile.c',
diff --git a/src/power-profiles-daemon.c b/src/power-profiles-daemon.c
index a0e0bd6..09193ed 100644
--- a/src/power-profiles-daemon.c
+++ b/src/power-profiles-daemon.c
@@ -89,6 +89,7 @@ static void start_profile_drivers (PpdApp *data);
/* profile drivers and actions */
#include "ppd-action-trickle-charge.h"
+#include "ppd-action-amdgpu-panel-power.h"
#include "ppd-driver-placeholder.h"
#include "ppd-driver-platform-profile.h"
#include "ppd-driver-intel-pstate.h"
@@ -109,6 +110,7 @@ static GTypeGetFunc objects[] = {
/* Actions */
ppd_action_trickle_charge_get_type,
+ ppd_action_amdgpu_panel_power_get_type,
};
typedef enum {
diff --git a/src/ppd-action-amdgpu-panel-power.c b/src/ppd-action-amdgpu-panel-power.c
new file mode 100644
index 0000000..b782d5a
--- /dev/null
+++ b/src/ppd-action-amdgpu-panel-power.c
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2024 Advanced Micro Devices
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published by
+ * the Free Software Foundation.
+ *
+ */
+
+#define G_LOG_DOMAIN "AmdgpuAction"
+
+#include "config.h"
+
+#include
+
+#include "ppd-action-amdgpu-panel-power.h"
+#include "ppd-profile.h"
+#include "ppd-utils.h"
+
+#define PROC_CPUINFO_PATH "/proc/cpuinfo"
+
+#define PANEL_POWER_SYSFS_NAME "amdgpu/panel_power_savings"
+
+#define UPOWER_DBUS_NAME "org.freedesktop.UPower"
+#define UPOWER_DBUS_PATH "/org/freedesktop/UPower"
+#define UPOWER_DBUS_INTERFACE "org.freedesktop.UPower"
+
+/**
+ * SECTION:ppd-action-amdgpu-panel-power
+ * @Short_description: Power savings for eDP connected displays
+ * @Title: AMDGPU Panel power action
+ *
+ * The AMDGPU panel power action utilizes the sysfs attribute present on some DRM
+ * connectors for amdgpu called "panel_power_savings". This will use an AMD specific
+ * hardware feature for a power savings profile for the panel.
+ *
+ */
+
+struct _PpdActionAmdgpuPanelPower
+{
+ PpdAction parent_instance;
+ PpdProfile last_profile;
+
+ GUdevClient *client;
+ GDBusProxy *proxy;
+ guint watcher_id;
+
+ gint panel_power_saving;
+ gboolean on_battery;
+};
+
+G_DEFINE_TYPE (PpdActionAmdgpuPanelPower, ppd_action_amdgpu_panel_power, PPD_TYPE_ACTION)
+
+static GObject*
+ppd_action_amdgpu_panel_power_constructor (GType type,
+ guint n_construct_params,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+
+ object = G_OBJECT_CLASS (ppd_action_amdgpu_panel_power_parent_class)->constructor (type,
+ n_construct_params,
+ construct_params);
+ g_object_set (object,
+ "action-name", "amdgpu_panel_power",
+ NULL);
+
+ return object;
+}
+
+static gboolean
+set_panel_power (PpdActionAmdgpuPanelPower *self, gint power, GError **error)
+{
+ GList *devices, *l;
+
+ devices = g_udev_client_query_by_subsystem (self->client, "drm");
+ if (devices == NULL) {
+ g_set_error_literal (error,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_FOUND,
+ "no drm devices found");
+ return FALSE;
+ }
+
+ for (l = devices; l != NULL; l = l->next) {
+ GUdevDevice *dev = l->data;
+ const char *value;
+ guint64 parsed;
+
+ value = g_udev_device_get_devtype (dev);
+ if (g_strcmp0 (value, "drm_connector") != 0)
+ continue;
+
+ value = g_udev_device_get_sysfs_attr_uncached (dev, PANEL_POWER_SYSFS_NAME);
+ if (!value)
+ continue;
+
+ parsed = g_ascii_strtoull (value, NULL, 10);
+
+ /* overflow check */
+ if (parsed == G_MAXUINT64) {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "cannot parse %s as caused overflow",
+ value);
+ return FALSE;
+ }
+
+ if (parsed == power)
+ continue;
+
+ if (!ppd_utils_write_sysfs_int (dev, PANEL_POWER_SYSFS_NAME, power, error))
+ return FALSE;
+
+ break;
+ }
+
+ g_list_free_full (devices, g_object_unref);
+
+ return TRUE;
+}
+
+static gboolean
+ppd_action_amdgpu_panel_update_target (PpdActionAmdgpuPanelPower *self,
+ GError **error)
+{
+ gint target = 0;
+
+ /* only activate if we know that we're on battery */
+ if (self->on_battery) {
+ switch (self->last_profile) {
+ case PPD_PROFILE_POWER_SAVER:
+ target = 4;
+ break;
+ case PPD_PROFILE_BALANCED:
+ target = 3;
+ break;
+ case PPD_PROFILE_PERFORMANCE:
+ target = 0;
+ break;
+ }
+ }
+
+ if (!set_panel_power (self, target, error))
+ return FALSE;
+ self->panel_power_saving = target;
+
+ return TRUE;
+}
+
+static gboolean
+ppd_action_amdgpu_panel_power_activate_profile (PpdAction *action,
+ PpdProfile profile,
+ GError **error)
+{
+ PpdActionAmdgpuPanelPower *self = PPD_ACTION_AMDGPU_PANEL_POWER (action);
+ self->last_profile = profile;
+
+ if (self->proxy == NULL) {
+ g_debug ("upower not available; battery data might be stale");
+ return TRUE;
+ }
+
+ return ppd_action_amdgpu_panel_update_target (self, error);
+}
+
+
+static void
+upower_properties_changed (GDBusProxy *proxy,
+ GVariant *changed_properties,
+ GStrv invalidated_properties,
+ PpdActionAmdgpuPanelPower *self)
+{
+ g_autoptr (GVariant) battery_val = NULL;
+ g_autoptr (GError) error = NULL;
+ gboolean new_on_battery;
+
+ if (proxy != NULL)
+ battery_val = g_dbus_proxy_get_cached_property (proxy, "OnBattery");
+ new_on_battery = battery_val ? g_variant_get_boolean (battery_val) : FALSE;
+
+ if (self->on_battery == new_on_battery)
+ return;
+
+ g_debug ("OnBattery: %d -> %d", self->on_battery, new_on_battery);
+ self->on_battery = new_on_battery;
+ if (!ppd_action_amdgpu_panel_update_target (self, &error))
+ g_warning ("failed to update target: %s", error->message);
+}
+
+static void
+udev_uevent_cb (GUdevClient *client,
+ gchar *action,
+ GUdevDevice *device,
+ gpointer user_data)
+{
+ PpdActionAmdgpuPanelPower *self = user_data;
+
+ if (!g_str_equal (action, "add"))
+ return;
+
+ if (!g_udev_device_has_sysfs_attr (device, PANEL_POWER_SYSFS_NAME))
+ return;
+
+ g_debug ("Updating panel power saving for '%s' to '%d'",
+ g_udev_device_get_sysfs_path (device),
+ self->panel_power_saving);
+ ppd_utils_write_sysfs_int (device, PANEL_POWER_SYSFS_NAME,
+ self->panel_power_saving, NULL);
+}
+
+static PpdProbeResult
+ppd_action_amdgpu_panel_power_probe (PpdAction *action)
+{
+ g_autofree gchar *cpuinfo_path = NULL;
+ g_autofree gchar *cpuinfo = NULL;
+ g_auto(GStrv) lines = NULL;
+
+ cpuinfo_path = ppd_utils_get_sysfs_path (PROC_CPUINFO_PATH);
+ if (!g_file_get_contents (cpuinfo_path, &cpuinfo, NULL, NULL))
+ return PPD_PROBE_RESULT_FAIL;
+
+ lines = g_strsplit (cpuinfo, "\n", -1);
+
+ for (gchar **line = lines; *line != NULL; line++) {
+ if (g_str_has_prefix (*line, "vendor_id") &&
+ strchr (*line, ':')) {
+ g_auto(GStrv) sections = g_strsplit (*line, ":", 2);
+
+ if (g_strv_length (sections) < 2)
+ continue;
+ if (g_strcmp0 (g_strstrip (sections[1]), "AuthenticAMD") == 0)
+ return PPD_PROBE_RESULT_SUCCESS;
+ }
+ }
+
+
+ return PPD_PROBE_RESULT_FAIL;
+}
+
+static void
+upower_name_vanished (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ PpdActionAmdgpuPanelPower *self = user_data;
+
+ g_debug ("%s vanished", UPOWER_DBUS_NAME);
+
+ /* reset */
+ g_clear_pointer (&self->proxy, g_object_unref);
+ upower_properties_changed (NULL, NULL, NULL, self);
+}
+
+static void
+upower_name_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ PpdActionAmdgpuPanelPower *self = user_data;
+ g_autoptr (GError) error = NULL;
+
+ g_debug ("%s appeared", UPOWER_DBUS_NAME);
+ self->proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ UPOWER_DBUS_NAME,
+ UPOWER_DBUS_PATH,
+ UPOWER_DBUS_INTERFACE,
+ NULL,
+ &error);
+ if (self->proxy == NULL) {
+ g_debug ("failed to connect to upower: %s", error->message);
+ return;
+ }
+
+ g_signal_connect (self->proxy,
+ "g-properties-changed",
+ G_CALLBACK(upower_properties_changed),
+ self);
+ upower_properties_changed (self->proxy, NULL, NULL, self);
+}
+
+static void
+ppd_action_amdgpu_panel_power_finalize (GObject *object)
+{
+ PpdActionAmdgpuPanelPower *action;
+
+ action = PPD_ACTION_AMDGPU_PANEL_POWER (object);
+ g_clear_handle_id (&action->watcher_id, g_bus_unwatch_name);
+ g_clear_object (&action->client);
+ g_clear_pointer (&action->proxy, g_object_unref);
+ G_OBJECT_CLASS (ppd_action_amdgpu_panel_power_parent_class)->finalize (object);
+}
+
+static void
+ppd_action_amdgpu_panel_power_class_init (PpdActionAmdgpuPanelPowerClass *klass)
+{
+ GObjectClass *object_class;
+ PpdActionClass *driver_class;
+
+ object_class = G_OBJECT_CLASS(klass);
+ object_class->constructor = ppd_action_amdgpu_panel_power_constructor;
+ object_class->finalize = ppd_action_amdgpu_panel_power_finalize;
+
+ driver_class = PPD_ACTION_CLASS(klass);
+ driver_class->probe = ppd_action_amdgpu_panel_power_probe;
+ driver_class->activate_profile = ppd_action_amdgpu_panel_power_activate_profile;
+}
+
+static void
+ppd_action_amdgpu_panel_power_init (PpdActionAmdgpuPanelPower *self)
+{
+ const gchar * const subsystem[] = { "drm", NULL };
+
+ self->client = g_udev_client_new (subsystem);
+ g_signal_connect (G_OBJECT (self->client), "uevent",
+ G_CALLBACK (udev_uevent_cb), self);
+
+ self->watcher_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM,
+ UPOWER_DBUS_NAME,
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ upower_name_appeared,
+ upower_name_vanished,
+ self,
+ NULL);
+}
diff --git a/src/ppd-action-amdgpu-panel-power.h b/src/ppd-action-amdgpu-panel-power.h
new file mode 100644
index 0000000..9c0e797
--- /dev/null
+++ b/src/ppd-action-amdgpu-panel-power.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2024 Advanced Micro Devices
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published by
+ * the Free Software Foundation.
+ *
+ */
+
+#pragma once
+
+#include "ppd-action.h"
+
+#define PPD_TYPE_ACTION_AMDGPU_PANEL_POWER (ppd_action_amdgpu_panel_power_get_type())
+G_DECLARE_FINAL_TYPE(PpdActionAmdgpuPanelPower, ppd_action_amdgpu_panel_power, PPD, ACTION_AMDGPU_PANEL_POWER, PpdAction)
diff --git a/tests/integration_test.py b/tests/integration_test.py
index 41c0edc..fca922a 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -320,6 +320,12 @@ class Tests(dbusmock.DBusTestCase):
["DEVPATH", "/devices/platform/thinkpad_acpi"],
)
+ def create_amd_apu(self):
+ proc_dir = os.path.join(self.testbed.get_root_dir(), "proc/")
+ os.makedirs(proc_dir)
+ with open(os.path.join(proc_dir, "cpuinfo"), "w", encoding="ASCII") as cpu:
+ cpu.write("vendor_id : AuthenticAMD\n")
+
def create_empty_platform_profile(self):
acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
os.makedirs(acpi_dir)
@@ -1358,6 +1364,56 @@ class Tests(dbusmock.DBusTestCase):
profiles = self.get_dbus_property("Profiles")
self.assertEqual(len(profiles), 2)
+ def test_amdgpu_panel_power(self):
+ """Verify AMDGPU Panel power actions"""
+ edp = self.testbed.add_device(
+ "drm",
+ "card1-eDP",
+ None,
+ ["amdgpu/panel_power_savings", "0"],
+ ["DEVTYPE", "drm_connector"],
+ )
+
+ self.create_amd_apu()
+
+ self.start_daemon()
+
+ self.assertIn("amdgpu_panel_power", self.get_dbus_property("Actions"))
+
+ # verify it hasn't been updated yet due to missing upower
+ self.assertEqual(self.read_sysfs_attr(edp, "amdgpu/panel_power_savings"), b"0")
+
+ # start upower and try again
+ self.stop_daemon()
+ self.spawn_server_template(
+ "upower",
+ {"DaemonVersion": "0.99", "OnBattery": True},
+ stdout=subprocess.PIPE,
+ )
+ self.start_daemon()
+
+ # verify balanced updated it
+ self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("balanced"))
+ self.assertEqual(self.read_sysfs_attr(edp, "amdgpu/panel_power_savings"), b"3")
+
+ # verify power saver updated it
+ self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+ self.assertEqual(self.read_sysfs_attr(edp, "amdgpu/panel_power_savings"), b"4")
+
+ # add another device that supports the feature
+ edp2 = self.testbed.add_device(
+ "drm",
+ "card2-eDP",
+ None,
+ ["amdgpu/panel_power_savings", "0"],
+ ["DEVTYPE", "drm_connector"],
+ )
+
+ # verify power saver got updated for it
+ self.assert_eventually(
+ lambda: self.read_sysfs_attr(edp2, "amdgpu/panel_power_savings") == b"4"
+ )
+
def test_trickle_charge_system(self):
"""Trickle power_supply charge type"""