* [PATCH v2 1/5] drm/amd/display: Render the panel power savings kernel-doc
2026-11-13 20:24 [PATCH v2 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
@ 2026-11-13 20:24 ` Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 2/5] drm/amd/display: Don't force a modeset when the ABM level changes Hugo Sales via B4 Relay
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales via B4 Relay @ 2026-11-13 20:24 UTC (permalink / raw)
To: Harry Wentland, Leo Li, Rodrigo Siqueira, Alex Deucher
Cc: Mario Limonciello, Ray Wu, Alex Hung, Bhawanpreet Lakha,
Ivan Lipski, Chenyu Chen, Christian König, David Airlie,
Simona Vetter, amd-gfx, dri-devel, linux-doc, linux-kernel,
Hugo Sales
From: Hugo Sales <hugo@hsal.es>
The DOC section for the panel_power_savings sysfs file is not included
in any rst file, so it never shows up in the built documentation. Add it
to the Display Properties section next to the 'adaptive backlight
modulation' property it interacts with
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
Documentation/gpu/amdgpu/display/display-manager.rst | 3 +++
1 file changed, 3 insertions(+)
diff --git a/Documentation/gpu/amdgpu/display/display-manager.rst b/Documentation/gpu/amdgpu/display/display-manager.rst
index f6de9e7779e29..ca4a956d2aa17 100644
--- a/Documentation/gpu/amdgpu/display/display-manager.rst
+++ b/Documentation/gpu/amdgpu/display/display-manager.rst
@@ -187,3 +187,6 @@ Display Properties
.. kernel-doc:: drivers/gpu/drm/amd/amdgpu/amdgpu_display.c
:doc: property for adaptive backlight modulation
+
+.. kernel-doc:: drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
+ :doc: panel power savings
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 2/5] drm/amd/display: Don't force a modeset when the ABM level changes
2026-11-13 20:24 [PATCH v2 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales via B4 Relay
@ 2026-11-13 20:24 ` Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe Hugo Sales via B4 Relay
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales via B4 Relay @ 2026-11-13 20:24 UTC (permalink / raw)
To: Harry Wentland, Leo Li, Rodrigo Siqueira, Alex Deucher
Cc: Mario Limonciello, Ray Wu, Alex Hung, Bhawanpreet Lakha,
Ivan Lipski, Chenyu Chen, Christian König, David Airlie,
Simona Vetter, amd-gfx, dri-devel, linux-doc, linux-kernel,
Hugo Sales
From: Hugo Sales <hugo@hsal.es>
An upcoming patch commits panel_power_savings writes from the kernel
without a modeset. amdgpu_dm_atomic_check() sets connectors_changed on the
CRTC when the connector's ABM level changes, so that commit would fail
with -EINVAL
The flag came in with commit c5892a102182 ("drm/amd/display: Fix dmesg
warning from setting abm level"), which also added the CRTC to the commit.
Setting the level alone left the CRTC out, so no stream update reached DC.
Adding the CRTC is enough for that. Since commit b05e2c5e81f9
("drm/amd/display: Update ABM crtc state on non-modeset"),
dm_update_crtc_state() copies the level from the connector to the CRTC
state on the non-modeset path, and the commit tail sends a changed level
as stream_update.abm_level. Neither needs the planes in the commit. The
commit tail walks the connectors and takes the surfaces from the stream
status in DC
This applies to every commit that changes the level. A compositor that
sets the "adaptive backlight modulation" KMS property no longer gets a
modeset either
The forced modeset also took the panel out of PSR or Panel Replay before
the level reached DC. The non-modeset path relies on "drm/amd/display:
Leave self-refresh before an ABM level update" for that, so it has to be
applied first
Keep the flag for scaling changes, which move the stream's source and
destination rectangles
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
This needs "drm/amd/display: Leave self-refresh before an ABM level
update" [1] applied first. Without it, a level change on the
non-modeset path reaches DC while PSR or Panel Replay is active
[1] https://lore.kernel.org/r/20261113-amdgpu-psr-abm-update-next-v2-0-27ce790856ea@hsal.es
Upstream, a level already reaches DC this way when a compositor commits
the connector after a panel_power_savings write. The store has already
written the level into the current connector state, so the old and new
connector states match and atomic check doesn't ask for a modeset. Only
the CRTC state, which still holds the old level, sees the change. With
this series, the hardware tests in the cover letter apply every level this
way
When a commit changes the level and also has planes on that CRTC, for
example a compositor that sets the property and flips in the same
commit, upstream sends the level to DC twice: from the connector loop in
the commit tail and from amdgpu_dm_commit_planes(). The same already
happens upstream after a panel_power_savings write, when the
compositor's next commit has both the connector and planes. [1] sends it
from the connector loop only
If a level change through the KMS property should still force a modeset,
the store could flag its own commits instead. It would set a flag in
dm_connector_state that atomic check reads to skip connectors_changed.
duplicate_state wouldn't copy the flag, so it only lasts for that one
commit. I think it's simpler to treat the change the same way whoever made
it, but I can send that version
Scaling has been in the same check since commit 3c6d1aeb4342
("drm/amd/display: Fix set scaling doesn's work"). From my reading the
non-modeset path handles it too, as a src/dst stream update, but it
changes the scaler ratio and I can't tell whether DC needs the full
validation for that. Could it go as well?
---
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
index dfbc384fbbdb4..256699cdd0b5c 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
@@ -6304,8 +6304,7 @@ STATIC_IFN_KUNIT int amdgpu_dm_atomic_check(struct drm_device *dev,
goto fail;
}
- if (dm_old_con_state->abm_level != dm_new_con_state->abm_level ||
- dm_old_con_state->scaling != dm_new_con_state->scaling)
+ if (dm_old_con_state->scaling != dm_new_con_state->scaling)
new_crtc_state->connectors_changed = true;
}
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe
2026-11-13 20:24 [PATCH v2 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 2/5] drm/amd/display: Don't force a modeset when the ABM level changes Hugo Sales via B4 Relay
@ 2026-11-13 20:24 ` Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 5/5] drm/amd/display: Test panel_power_savings commits Hugo Sales via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales via B4 Relay @ 2026-11-13 20:24 UTC (permalink / raw)
To: Harry Wentland, Leo Li, Rodrigo Siqueira, Alex Deucher
Cc: Mario Limonciello, Ray Wu, Alex Hung, Bhawanpreet Lakha,
Ivan Lipski, Chenyu Chen, Christian König, David Airlie,
Simona Vetter, amd-gfx, dri-devel, linux-doc, linux-kernel,
Hugo Sales
From: Hugo Sales <hugo@hsal.es>
An upcoming patch makes the store apply the level with an atomic commit.
The store tests allocate their connector with kunit_kzalloc() instead of
initialising it on the DRM device, and have no CRTC, plane or encoder, so
a commit can't run on it
Build a primary plane, a CRTC, an encoder and an eDP connector on the
KUnit device instead, with runtime PM enabled as on a probed device, and
move the two store tests onto it. They check the same results as before.
The connector reset already leaves the disable sentinel, so the disable
test sets level 2 before the write
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
.../amdgpu_dm/tests/amdgpu_dm_backlight_test.c | 90 ++++++++++++++++++++--
1 file changed, 84 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
index c6d15fb8ad989..5133c8f251f53 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
@@ -7,10 +7,14 @@
#include <kunit/test.h>
#include <linux/backlight.h>
+#include <linux/pm_runtime.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_connector.h>
+#include <drm/drm_kunit_helpers.h>
+#include <drm/drm_managed.h>
#include <drm/drm_mode_config.h>
+#include <drm/drm_modeset_helper_vtables.h>
#include <drm/drm_property.h>
#include "dc.h"
@@ -544,6 +548,76 @@ static struct drm_connector *setup_panel_power_savings_connector(struct kunit *t
return connector;
}
+static const struct drm_connector_funcs dm_test_pps_connector_funcs = {
+ .reset = amdgpu_dm_connector_funcs_reset,
+ .atomic_duplicate_state = amdgpu_dm_connector_atomic_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+};
+
+static int dm_test_pps_get_modes(struct drm_connector *connector)
+{
+ return 0;
+}
+
+/* update_connector_routing() dereferences the helper funcs unconditionally */
+static const struct drm_connector_helper_funcs dm_test_pps_connector_helper_funcs = {
+ .get_modes = dm_test_pps_get_modes,
+};
+
+/*
+ * Build a primary plane, a CRTC, an encoder and an eDP connector that the
+ * store can commit to. The KUnit device uses the generic atomic helpers, so a
+ * commit runs drm_atomic_helper_check() and drm_atomic_helper_commit() rather
+ * than the DM implementations
+ */
+static struct drm_connector *
+setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
+{
+ struct amdgpu_dm_connector *aconnector;
+ struct drm_connector *connector;
+ struct drm_encoder *encoder;
+ struct amdgpu_device *adev;
+ struct drm_plane *primary;
+ struct drm_crtc *crtc;
+ struct device *device;
+
+ adev = dm_kunit_alloc_adev(test);
+ KUNIT_ASSERT_EQ(test, pm_runtime_set_active(adev->ddev.dev), 0);
+ KUNIT_ASSERT_EQ(test, devm_pm_runtime_enable(adev->ddev.dev), 0);
+
+ primary = drm_kunit_helper_create_primary_plane(test, &adev->ddev, NULL,
+ NULL, NULL, 0, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, primary);
+ crtc = drm_kunit_helper_create_crtc(test, &adev->ddev, primary, NULL,
+ NULL, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, crtc);
+
+ encoder = drmm_kzalloc(&adev->ddev, sizeof(*encoder), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, encoder);
+ KUNIT_ASSERT_EQ(test, drmm_encoder_init(&adev->ddev, encoder, NULL,
+ DRM_MODE_ENCODER_TMDS, NULL), 0);
+ encoder->possible_crtcs = drm_crtc_mask(crtc);
+
+ aconnector = drmm_kzalloc(&adev->ddev, sizeof(*aconnector), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, aconnector);
+ connector = &aconnector->base;
+ KUNIT_ASSERT_EQ(test, drmm_connector_init(&adev->ddev, connector,
+ &dm_test_pps_connector_funcs,
+ DRM_MODE_CONNECTOR_eDP, NULL), 0);
+ drm_connector_helper_add(connector, &dm_test_pps_connector_helper_funcs);
+ KUNIT_ASSERT_EQ(test, drm_connector_attach_encoder(connector, encoder), 0);
+
+ drm_mode_config_reset(&adev->ddev);
+ KUNIT_ASSERT_NOT_NULL(test, connector->state);
+
+ device = kunit_kzalloc(test, sizeof(*device), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, device);
+ dev_set_drvdata(device, connector);
+ *device_out = device;
+
+ return connector;
+}
+
static void dm_test_free_sysfs_buf(void *data)
{
free_page((unsigned long)data);
@@ -607,15 +681,17 @@ static void dm_test_panel_power_savings_show_reports_level(struct kunit *test)
*/
static void dm_test_panel_power_savings_store_sets_disable(struct kunit *test)
{
- struct dm_connector_state *state;
+ struct drm_connector *connector;
struct device *device;
size_t count = strlen("0");
- setup_panel_power_savings_connector(test, &device, &state);
+ connector = setup_panel_power_savings_pipe(test, &device);
+ to_dm_connector_state(connector->state)->abm_level = 2;
KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "0", count),
(ssize_t)count);
- KUNIT_EXPECT_EQ(test, state->abm_level, ABM_LEVEL_IMMEDIATE_DISABLE);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level,
+ ABM_LEVEL_IMMEDIATE_DISABLE);
}
/**
@@ -624,11 +700,13 @@ static void dm_test_panel_power_savings_store_sets_disable(struct kunit *test)
*/
static void dm_test_panel_power_savings_store_forbidden(struct kunit *test)
{
- struct dm_connector_state *state;
+ struct dm_connector_state *dm_state;
+ struct drm_connector *connector;
struct device *device;
- setup_panel_power_savings_connector(test, &device, &state);
- state->abm_sysfs_forbidden = true;
+ connector = setup_panel_power_savings_pipe(test, &device);
+ dm_state = to_dm_connector_state(connector->state);
+ dm_state->abm_sysfs_forbidden = true;
KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "1", 1), -EBUSY);
}
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel
2026-11-13 20:24 [PATCH v2 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
` (2 preceding siblings ...)
2026-11-13 20:24 ` [PATCH v2 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe Hugo Sales via B4 Relay
@ 2026-11-13 20:24 ` Hugo Sales via B4 Relay
2026-11-13 20:24 ` [PATCH v2 5/5] drm/amd/display: Test panel_power_savings commits Hugo Sales via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales via B4 Relay @ 2026-11-13 20:24 UTC (permalink / raw)
To: Harry Wentland, Leo Li, Rodrigo Siqueira, Alex Deucher
Cc: Mario Limonciello, Ray Wu, Alex Hung, Bhawanpreet Lakha,
Ivan Lipski, Chenyu Chen, Christian König, David Airlie,
Simona Vetter, amd-gfx, dri-devel, linux-doc, linux-kernel,
Hugo Sales
From: Hugo Sales <hugo@hsal.es>
panel_power_savings_store() currently writes the level into
connector->state and sends a hotplug uevent, and the hardware only gets it
on a later atomic commit that includes the connector. Hyprland adds the
connector to a commit only on a modeset or when a connector property it
sets changes, so it never applies the level
Programs that write the sysfs file, such as power-profiles-daemon, aren't
the DRM master and can't commit themselves, so they depend on the master
taking the uevent as a cue to commit the connector, but nothing documents
they should. A master that wants to control ABM sets the 'adaptive
backlight modulation' property, and from then on writes return -EBUSY. The
file has to work without the master's help, like the backlight device,
whose writes reach DC without a commit
Commit the level from the store instead, with a duplicated connector state
and no modeset. The write returns once the commit is done, or with the
commit's error, so no uevent is needed. A connector that is off gets the
level when it is next enabled. A write of the level the connector already
has, or one the DRM master forbids, returns without a commit and without
waking a runtime-suspended GPU
Document the new behaviour in the kernel-doc
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
This needs "drm/amd/display: Don't force a modeset when the ABM level
changes". Without it, atomic check sees the changed level, asks for a
modeset and the commit fails with -EINVAL
A failed write used to leave the level stored and send the uevent anyway.
Now the level only changes through the commit, so the store reports the
error. power-profiles-daemon logs a failed write and tries again on the
next profile, AC or battery change, because it compares against the value
it reads back. It ignores the hotplug uevent. Compositors used it only as
a cue to commit the connector, which the store now does itself
The commit is blocking, so a write can hold the CRTC locks for up to a
frame while a compositor commit waits. The other way round, while the
store's commit is pending, a nonblocking commit from the compositor on the
same CRTC fails with -EBUSY, as it would behind any pending commit
The store takes a runtime PM reference around the commit, like the
atomic ioctl path. Its locks are interruptible, and a write a signal
interrupts returns -ERESTARTSYS so that it restarts
---
.../amd/display/amdgpu_dm/amdgpu_dm_backlight.c | 118 +++++++++++++++++++--
1 file changed, 107 insertions(+), 11 deletions(-)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
index 27546febf90d3..8487ac0661396 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
@@ -40,7 +40,10 @@
#include <linux/backlight.h>
#include <linux/minmax.h>
+#include <linux/pm_runtime.h>
#include <linux/power_supply.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_drv.h>
#include <drm/drm_edid.h>
#include <drm/drm_utils.h>
@@ -691,6 +694,18 @@ EXPORT_IF_KUNIT(amdgpu_dm_setup_backlight_device);
*
* Modifying this value can have implications on color accuracy, so tread
* carefully.
+ *
+ * A write commits the new level before it returns, without a modeset. The
+ * commit waits for any commit already pending on the connector's CRTC, and
+ * while it is pending, a nonblocking commit from the DRM master on that CRTC
+ * fails with -EBUSY. If the connector or its CRTC is off, the driver stores
+ * the level and applies it when the connector is next enabled
+ *
+ * If the commit fails, the write returns its error and the level stays as it
+ * was. While the CRTC needs a modeset, as after a DSC override in debugfs,
+ * writes fail with -EINVAL until a commit that allows a modeset clears that
+ * state. Writes fail with -EBUSY once the DRM master has set the connector's
+ * 'adaptive backlight modulation' property to a value other than 'sysfs'
*/
STATIC_IFN_KUNIT
@@ -712,13 +727,100 @@ ssize_t panel_power_savings_show(struct device *device,
}
EXPORT_IF_KUNIT(panel_power_savings_show);
+static int panel_power_savings_apply(struct drm_connector *connector,
+ unsigned int level)
+{
+ struct drm_connector_state *conn_state;
+ struct dm_connector_state *dm_state;
+ struct drm_device *dev = connector->dev;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_atomic_commit *state;
+ bool forbidden, unchanged;
+ int ret, idx;
+
+ level = level ?: ABM_LEVEL_IMMEDIATE_DISABLE;
+
+ if (!drm_dev_enter(dev, &idx))
+ return -ENODEV;
+
+ /*
+ * Don't wake the GPU for a write that changes nothing. The duplicated
+ * state below is checked again. This check only spares the runtime resume
+ */
+ ret = drm_modeset_lock_single_interruptible(&dev->mode_config.connection_mutex);
+ if (ret)
+ goto dev_exit;
+ dm_state = to_dm_connector_state(connector->state);
+ forbidden = dm_state->abm_sysfs_forbidden;
+ unchanged = dm_state->abm_level == level;
+ drm_modeset_unlock(&dev->mode_config.connection_mutex);
+
+ if (forbidden || unchanged) {
+ ret = forbidden ? -EBUSY : 0;
+ goto dev_exit;
+ }
+
+ ret = pm_runtime_resume_and_get(dev->dev);
+ if (ret < 0)
+ goto dev_exit;
+
+ state = drm_atomic_commit_alloc(dev);
+ if (!state) {
+ ret = -ENOMEM;
+ goto pm_put;
+ }
+
+ /* Kernel commits allow a modeset by default, which a level change doesn't need */
+ state->allow_modeset = false;
+
+ drm_modeset_acquire_init(&ctx, DRM_MODESET_ACQUIRE_INTERRUPTIBLE);
+ state->acquire_ctx = &ctx;
+retry:
+ conn_state = drm_atomic_get_connector_state(state, connector);
+ if (IS_ERR(conn_state)) {
+ ret = PTR_ERR(conn_state);
+ goto out;
+ }
+
+ dm_state = to_dm_connector_state(conn_state);
+ if (dm_state->abm_sysfs_forbidden) {
+ ret = -EBUSY;
+ goto out;
+ }
+
+ if (dm_state->abm_level == level) {
+ ret = 0;
+ goto out;
+ }
+
+ dm_state->abm_level = level;
+
+ ret = drm_atomic_commit(state);
+out:
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+
+ drm_atomic_commit_put(state);
+ drm_modeset_drop_locks(&ctx);
+ drm_modeset_acquire_fini(&ctx);
+pm_put:
+ pm_runtime_put_autosuspend(dev->dev);
+dev_exit:
+ drm_dev_exit(idx);
+
+ return ret;
+}
+
STATIC_IFN_KUNIT
ssize_t panel_power_savings_store(struct device *device,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct drm_connector *connector = dev_get_drvdata(device);
- struct drm_device *dev = connector->dev;
long val;
int ret;
@@ -730,19 +832,13 @@ ssize_t panel_power_savings_store(struct device *device,
if (val < 0 || val > 4)
return -EINVAL;
- drm_modeset_lock(&dev->mode_config.connection_mutex, NULL);
- if (to_dm_connector_state(connector->state)->abm_sysfs_forbidden)
- ret = -EBUSY;
- else
- to_dm_connector_state(connector->state)->abm_level = val ?:
- ABM_LEVEL_IMMEDIATE_DISABLE;
- drm_modeset_unlock(&dev->mode_config.connection_mutex);
-
+ ret = panel_power_savings_apply(connector, val);
+ /* The modeset locks return -EINTR, which write(2) would not restart */
+ if (ret == -EINTR)
+ return -ERESTARTSYS;
if (ret)
return ret;
- drm_kms_helper_hotplug_event(dev);
-
return count;
}
EXPORT_IF_KUNIT(panel_power_savings_store);
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 5/5] drm/amd/display: Test panel_power_savings commits
2026-11-13 20:24 [PATCH v2 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
` (3 preceding siblings ...)
2026-11-13 20:24 ` [PATCH v2 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales via B4 Relay
@ 2026-11-13 20:24 ` Hugo Sales via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales via B4 Relay @ 2026-11-13 20:24 UTC (permalink / raw)
To: Harry Wentland, Leo Li, Rodrigo Siqueira, Alex Deucher
Cc: Mario Limonciello, Ray Wu, Alex Hung, Bhawanpreet Lakha,
Ivan Lipski, Chenyu Chen, Christian König, David Airlie,
Simona Vetter, amd-gfx, dri-devel, linux-doc, linux-kernel,
Hugo Sales
From: Hugo Sales <hugo@hsal.es>
Add KUnit cases for a panel_power_savings write to a connector bound to a
CRTC. With the CRTC on or off, the store commits a connector state that
carries the level. When atomic check rejects the commit, the write returns
the error and the connector keeps its old level. A write of the current
level succeeds without a commit. The existing store tests also check that a
write replaces the connector state, and that a forbidden write keeps the
state and level. A forbidden write and a write of the current level leave a
runtime-suspended GPU suspended. For the new cases, the pipe helper can
route the connector to the CRTC, set whether the CRTC is active and take
CRTC helper funcs
The store checks the forbidden flag and the level again on the duplicated
connector state. Those checks only fire when a commit changes the level
between the first check and the commit's locks, which these tests can't
set up
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
.../amdgpu_dm/tests/amdgpu_dm_backlight_test.c | 195 ++++++++++++++++++++-
1 file changed, 188 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
index 5133c8f251f53..2484788946d54 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_backlight_test.c
@@ -10,6 +10,7 @@
#include <linux/pm_runtime.h>
#include <drm/drm_atomic_helper.h>
+#include <drm/drm_atomic_uapi.h>
#include <drm/drm_connector.h>
#include <drm/drm_kunit_helpers.h>
#include <drm/drm_managed.h>
@@ -564,16 +565,43 @@ static const struct drm_connector_helper_funcs dm_test_pps_connector_helper_func
.get_modes = dm_test_pps_get_modes,
};
+static int dm_test_pps_crtc_atomic_check_fail(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ return -ENOSPC;
+}
+
+static const struct drm_crtc_helper_funcs dm_test_pps_crtc_helper_funcs_fail = {
+ .atomic_check = dm_test_pps_crtc_atomic_check_fail,
+};
+
+/* The generic helpers accept a commit that allows a modeset it doesn't need */
+static int dm_test_pps_crtc_atomic_check_no_modeset(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ return state->allow_modeset ? -EINVAL : 0;
+}
+
+static const struct drm_crtc_helper_funcs dm_test_pps_crtc_helper_funcs_no_modeset = {
+ .atomic_check = dm_test_pps_crtc_atomic_check_no_modeset,
+};
+
/*
* Build a primary plane, a CRTC, an encoder and an eDP connector that the
- * store can commit to. The KUnit device uses the generic atomic helpers, so a
- * commit runs drm_atomic_helper_check() and drm_atomic_helper_commit() rather
- * than the DM implementations
+ * store can commit to. With @bound, the connector's current state is routed
+ * to the CRTC, which is enabled and has @active as its active state. The KUnit
+ * device uses the generic atomic helpers, so a commit runs
+ * drm_atomic_helper_check() and drm_atomic_helper_commit() rather than the DM
+ * implementations. A NULL @crtc_helper_funcs gives the KUnit helper's defaults
*/
static struct drm_connector *
-setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
+setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out,
+ bool bound, bool active,
+ const struct drm_crtc_helper_funcs *crtc_helper_funcs)
{
struct amdgpu_dm_connector *aconnector;
+ struct drm_display_mode *mode;
+ struct drm_crtc_state *crtc_state;
struct drm_connector *connector;
struct drm_encoder *encoder;
struct amdgpu_device *adev;
@@ -589,7 +617,7 @@ setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
NULL, NULL, 0, NULL);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, primary);
crtc = drm_kunit_helper_create_crtc(test, &adev->ddev, primary, NULL,
- NULL, NULL);
+ NULL, crtc_helper_funcs);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, crtc);
encoder = drmm_kzalloc(&adev->ddev, sizeof(*encoder), GFP_KERNEL);
@@ -610,6 +638,21 @@ setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
drm_mode_config_reset(&adev->ddev);
KUNIT_ASSERT_NOT_NULL(test, connector->state);
+ if (bound) {
+ /* A connector state bound to a CRTC holds a connector reference */
+ connector->state->crtc = crtc;
+ drm_connector_get(connector);
+ connector->state->best_encoder = encoder;
+ crtc_state = crtc->state;
+ crtc_state->connector_mask = drm_connector_mask(connector);
+ crtc_state->encoder_mask = drm_encoder_mask(encoder);
+ crtc_state->enable = true;
+ crtc_state->active = active;
+ mode = drm_kunit_display_mode_from_cea_vic(test, &adev->ddev, 1);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_ASSERT_EQ(test, drm_atomic_set_mode_for_crtc(crtc_state, mode), 0);
+ }
+
device = kunit_kzalloc(test, sizeof(*device), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, device);
dev_set_drvdata(device, connector);
@@ -618,6 +661,35 @@ setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
return connector;
}
+static void dm_test_pps_expect_no_pm_reference(struct kunit *test,
+ struct drm_connector *connector)
+{
+#ifdef CONFIG_PM
+ KUNIT_EXPECT_EQ(test, atomic_read(&connector->dev->dev->power.usage_count), 0);
+#endif
+}
+
+static void dm_test_pps_runtime_suspend(struct kunit *test,
+ struct drm_connector *connector)
+{
+#ifdef CONFIG_PM
+ struct device *dev = connector->dev->dev;
+
+ pm_runtime_disable(dev);
+ KUNIT_ASSERT_EQ(test, pm_runtime_set_suspended(dev), 0);
+ pm_runtime_enable(dev);
+#endif
+}
+
+static void dm_test_pps_expect_still_suspended(struct kunit *test,
+ struct drm_connector *connector)
+{
+#ifdef CONFIG_PM
+ KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(connector->dev->dev));
+#endif
+ dm_test_pps_expect_no_pm_reference(test, connector);
+}
+
static void dm_test_free_sysfs_buf(void *data)
{
free_page((unsigned long)data);
@@ -678,18 +750,24 @@ static void dm_test_panel_power_savings_show_reports_level(struct kunit *test)
/**
* dm_test_panel_power_savings_store_sets_disable - Test zero maps to disable
* @test: The KUnit test context
+ *
+ * With no CRTC on the connector, the store commits a connector state that
+ * carries the disable sentinel
*/
static void dm_test_panel_power_savings_store_sets_disable(struct kunit *test)
{
+ struct drm_connector_state *old_state;
struct drm_connector *connector;
struct device *device;
size_t count = strlen("0");
- connector = setup_panel_power_savings_pipe(test, &device);
+ connector = setup_panel_power_savings_pipe(test, &device, false, false, NULL);
to_dm_connector_state(connector->state)->abm_level = 2;
+ old_state = connector->state;
KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "0", count),
(ssize_t)count);
+ KUNIT_EXPECT_PTR_NE(test, connector->state, old_state);
KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level,
ABM_LEVEL_IMMEDIATE_DISABLE);
}
@@ -697,6 +775,9 @@ static void dm_test_panel_power_savings_store_sets_disable(struct kunit *test)
/**
* dm_test_panel_power_savings_store_forbidden - Test forbidden update
* @test: The KUnit test context
+ *
+ * Once the DRM master owns the level, the store fails without committing and
+ * without waking the runtime-suspended GPU
*/
static void dm_test_panel_power_savings_store_forbidden(struct kunit *test)
{
@@ -704,11 +785,16 @@ static void dm_test_panel_power_savings_store_forbidden(struct kunit *test)
struct drm_connector *connector;
struct device *device;
- connector = setup_panel_power_savings_pipe(test, &device);
+ connector = setup_panel_power_savings_pipe(test, &device, false, false, NULL);
dm_state = to_dm_connector_state(connector->state);
dm_state->abm_sysfs_forbidden = true;
+ dm_state->abm_level = 2;
+ dm_test_pps_runtime_suspend(test, connector);
KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "1", 1), -EBUSY);
+ KUNIT_EXPECT_PTR_EQ(test, connector->state, &dm_state->base);
+ KUNIT_EXPECT_EQ(test, dm_state->abm_level, 2);
+ dm_test_pps_expect_still_suspended(test, connector);
}
/**
@@ -757,6 +843,98 @@ static void dm_test_panel_power_savings_store_rejects_out_of_range(struct kunit
KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "5", 1), -EINVAL);
}
+struct dm_pps_store_crtc_param {
+ const char *name;
+ bool active;
+ const char *buf;
+ int level;
+};
+
+static const struct dm_pps_store_crtc_param dm_pps_store_crtc_params[] = {
+ { "inactive", false, "3", 3 },
+ { "active", true, "2", 2 },
+};
+
+KUNIT_ARRAY_PARAM_DESC(dm_pps_store_crtc, dm_pps_store_crtc_params, name);
+
+/**
+ * dm_test_panel_power_savings_store_crtc - Test store with the CRTC on or off
+ * @test: The KUnit test context
+ *
+ * With the connector's CRTC enabled, active or not, the store commits a
+ * connector state that carries the level, without allowing a modeset, and
+ * drops its runtime PM reference
+ */
+static void dm_test_panel_power_savings_store_crtc(struct kunit *test)
+{
+ const struct dm_pps_store_crtc_param *param = test->param_value;
+ struct drm_connector_state *old_state;
+ struct drm_connector *connector;
+ struct device *device;
+
+ connector = setup_panel_power_savings_pipe(test, &device, true, param->active,
+ &dm_test_pps_crtc_helper_funcs_no_modeset);
+ old_state = connector->state;
+
+ KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, param->buf, 1), 1);
+ KUNIT_EXPECT_PTR_NE(test, connector->state, old_state);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level,
+ param->level);
+ KUNIT_EXPECT_FALSE(test, drm_atomic_crtc_needs_modeset(connector->state->crtc->state));
+ dm_test_pps_expect_no_pm_reference(test, connector);
+}
+
+/**
+ * dm_test_panel_power_savings_store_commit_fails - Test store when the commit fails
+ * @test: The KUnit test context
+ *
+ * When atomic check rejects the commit, the write returns the error, the
+ * current connector state keeps the old level and the runtime PM reference is
+ * dropped
+ */
+static void dm_test_panel_power_savings_store_commit_fails(struct kunit *test)
+{
+ struct drm_connector_state *old_state;
+ struct drm_connector *connector;
+ struct device *device;
+
+ connector = setup_panel_power_savings_pipe(test, &device, true, true,
+ &dm_test_pps_crtc_helper_funcs_fail);
+ to_dm_connector_state(connector->state)->abm_level = 1;
+ old_state = connector->state;
+
+ KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "4", 1), -ENOSPC);
+ KUNIT_EXPECT_PTR_EQ(test, connector->state, old_state);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level, 1);
+ dm_test_pps_expect_no_pm_reference(test, connector);
+}
+
+/**
+ * dm_test_panel_power_savings_store_same_level - Test store of the current level
+ * @test: The KUnit test context
+ *
+ * Writing the level the connector already has succeeds without a commit, so
+ * the connector keeps its state even though atomic check would fail, and the
+ * runtime-suspended GPU stays suspended
+ */
+static void dm_test_panel_power_savings_store_same_level(struct kunit *test)
+{
+ struct drm_connector_state *old_state;
+ struct drm_connector *connector;
+ struct device *device;
+
+ connector = setup_panel_power_savings_pipe(test, &device, true, true,
+ &dm_test_pps_crtc_helper_funcs_fail);
+ to_dm_connector_state(connector->state)->abm_level = 2;
+ old_state = connector->state;
+ dm_test_pps_runtime_suspend(test, connector);
+
+ KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "2", 1), 1);
+ KUNIT_EXPECT_PTR_EQ(test, connector->state, old_state);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level, 2);
+ dm_test_pps_expect_still_suspended(test, connector);
+}
+
/* Tests for amdgpu_dm_backlight_get_device_index() */
/**
@@ -2019,6 +2197,9 @@ static struct kunit_case dm_backlight_test_cases[] = {
KUNIT_CASE(dm_test_panel_power_savings_store_forbidden),
KUNIT_CASE(dm_test_panel_power_savings_store_rejects_invalid_text),
KUNIT_CASE(dm_test_panel_power_savings_store_rejects_out_of_range),
+ KUNIT_CASE_PARAM(dm_test_panel_power_savings_store_crtc, dm_pps_store_crtc_gen_params),
+ KUNIT_CASE(dm_test_panel_power_savings_store_commit_fails),
+ KUNIT_CASE(dm_test_panel_power_savings_store_same_level),
/* amdgpu_dm_backlight_get_device_index */
KUNIT_CASE(dm_test_backlight_device_index_matches_second),
KUNIT_CASE(dm_test_backlight_device_index_missing_fallback),
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread