* [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel
@ 2026-11-02 10:17 Hugo Sales
2026-11-02 10:17 ` [PATCH 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
A write to panel_power_savings stores the ABM level and sends a hotplug
uevent, but the level reaches the hardware only on the next atomic commit
that includes the eDP connector. Hyprland adds the connector to a commit
only on a modeset or when a connector property it sets changes, so on my
laptop the file read back the new level and the backlight never changed.
power-profiles-daemon writes this file on every profile and AC change, so
none of its ABM changes applied
This shouldn't need a fix in every compositor. The file is for writers
that aren't the DRM master, like power-profiles-daemon, and they can't
commit anything themselves. Today they depend on the master taking a
generic hotplug uevent as a cue to commit the connector again. Nothing
documents that, and a compositor has no reason to read a hotplug event as
"a property you never touched has changed". A compositor that wants to own
ABM sets the 'adaptive backlight modulation' property, and from then on
the file returns -EBUSY. The file has to work without the master's help.
The backlight device already does: a brightness write goes straight to DC,
with no commit at all
This series makes the write commit the level itself:
1. Render the existing kernel-doc, which no rst file included
2. Stop forcing a modeset when the connector's ABM level changes. DM
already sends a new level as a stream update, and the modeset flag
dates from before the CRTC was added to such commits
3. Move the store tests onto a full pipe the store can commit to
4. Commit the level from the store through a duplicated connector
state. The write returns the commit's error and no longer sends a
uevent
5. KUnit cases for a connector with the CRTC on, off and failing
atomic check
Patch 2 also asks, below its cut, whether scaling can leave the same
check
v1 of the patch that added this file had no flush [1], and Mario found
that the level only took effect on the next modeset [2]. v2's changelog
says "force an atomic commit" [3], but the merged code (63d0b87213a0)
sends a hotplug event instead. 8d20a066fa9b later applied the level
outside atomic and was reverted in 95aaa207e9ef because it broke igt
abm_gradual. The objections back then were to sysfs overriding the DRM
master at all [4], which the property above now covers. I didn't find
any discussion that rejects an internal atomic commit
A write now returns the commit's error instead of storing a level the
hardware may never get. power-profiles-daemon logs the error and tries
again on its next profile, AC or battery change. Nothing I found reads
the uevent for more than a cue to commit the connector, which the kernel
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
Tested on my laptop (ASUS ROG Flow X13 GV301QH, DCN 2.1) with the series
on 7.3:
- igt amd_abm passes all five subtests
- Under Hyprland, with no modeset during the run, 25 writes cycling
levels 0 to 4 all succeeded and read back, with no -EBUSY and nothing
in the kernel log. A write sends no uevent
- The backlight PWM follows each write. At level 4 the target PWM is
43% of level 0 at the same brightness, and level 0 restores it
[1] https://lore.kernel.org/amd-gfx/20240126222300.119292-1-hamza.mahfooz@amd.com/
[2] https://lore.kernel.org/amd-gfx/1162e59e-3f53-4c84-b186-bb2f4642216b@amd.com/
[3] https://lore.kernel.org/amd-gfx/20240202152837.7388-1-hamza.mahfooz@amd.com/
[4] https://lore.kernel.org/amd-gfx/b117ccc0-6569-4753-9cb9-c304f9e50d30@amd.com/
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
Hugo Sales (5):
drm/amd/display: Render the panel power savings kernel-doc
drm/amd/display: Don't force a modeset when the ABM level changes
drm/amd/display: Give the panel_power_savings store tests a full pipe
drm/amd/display: Commit panel_power_savings writes from the kernel
drm/amd/display: Test panel_power_savings commits
.../gpu/amdgpu/display/display-manager.rst | 3 +
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c | 3 +-
.../amd/display/amdgpu_dm/amdgpu_dm_backlight.c | 90 +++++++-
.../amdgpu_dm/tests/amdgpu_dm_backlight_test.c | 231 ++++++++++++++++++++-
4 files changed, 308 insertions(+), 19 deletions(-)
---
base-commit: ba496bb837882eb1af58f41572fc289e46c77b8c
change-id: 20261101-abm-sysfs-commit-96a38de1ac66
Best regards,
--
Hugo Sales <hugo@hsal.es>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/5] drm/amd/display: Render the panel power savings kernel-doc
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
@ 2026-11-02 10:17 ` Hugo Sales
2026-11-02 10:17 ` [PATCH 2/5] drm/amd/display: Don't force a modeset when the ABM level changes Hugo Sales
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
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 related [flat|nested] 6+ messages in thread
* [PATCH 2/5] drm/amd/display: Don't force a modeset when the ABM level changes
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
2026-11-02 10:17 ` [PATCH 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales
@ 2026-11-02 10:17 ` Hugo Sales
2026-11-02 10:17 ` [PATCH 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe Hugo Sales
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
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"), because setting the level did not add
the CRTC to the commit and no stream update reached DC. With other changes
since then, that fix is no longer needed. On the non-modeset path,
dm_update_crtc_state() copies the level from the connector to the CRTC
state and the commit tail sends a changed level as stream_update.abm_level
Keep the flag for scaling changes, which move the stream's source and
destination rectangles
Signed-off-by: Hugo Sales <hugo@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
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.
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 related [flat|nested] 6+ messages in thread
* [PATCH 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
2026-11-02 10:17 ` [PATCH 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales
2026-11-02 10:17 ` [PATCH 2/5] drm/amd/display: Don't force a modeset when the ABM level changes Hugo Sales
@ 2026-11-02 10:17 ` Hugo Sales
2026-11-02 10:17 ` [PATCH 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
2026-11-02 10:17 ` [PATCH 5/5] drm/amd/display: Test panel_power_savings commits Hugo Sales
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
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
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
.../amdgpu_dm/tests/amdgpu_dm_backlight_test.c | 88 ++++++++++++++++++++--
1 file changed, 82 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..35e245b078f7a 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,75 @@ 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);
+
+ 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 +680,16 @@ 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);
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 +698,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 related [flat|nested] 6+ messages in thread
* [PATCH 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
` (2 preceding siblings ...)
2026-11-02 10:17 ` [PATCH 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe Hugo Sales
@ 2026-11-02 10:17 ` Hugo Sales
2026-11-02 10:17 ` [PATCH 5/5] drm/amd/display: Test panel_power_savings commits Hugo Sales
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
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 level is applied, or with the
commit's error, so no uevent is needed. A connector that is off gets the
level when it is next enabled
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, and its locks are interruptible
---
.../amd/display/amdgpu_dm/amdgpu_dm_backlight.c | 90 +++++++++++++++++++---
1 file changed, 79 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..a661fd558686f 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,75 @@ 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;
+ int ret, idx;
+
+ if (!drm_dev_enter(dev, &idx))
+ return -ENODEV;
+
+ 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 never needs */
+ 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;
+ }
+
+ dm_state->abm_level = level ?: ABM_LEVEL_IMMEDIATE_DISABLE;
+
+ 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 +807,10 @@ 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);
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 related [flat|nested] 6+ messages in thread
* [PATCH 5/5] drm/amd/display: Test panel_power_savings commits
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
` (3 preceding siblings ...)
2026-11-02 10:17 ` [PATCH 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
@ 2026-11-02 10:17 ` Hugo Sales
4 siblings, 0 replies; 6+ messages in thread
From: Hugo Sales @ 2026-11-02 10:17 UTC (permalink / raw)
To: sunpeng.li, siqueira, amd-gfx, alexander.deucher, harry.wentland
Cc: Hugo Sales, airlied, linux-kernel, mario.limonciello,
christian.koenig, simona, linux-doc, bhawanpreet.lakha,
ivan.lipski, dri-devel, ray.wu, alex.hung
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. The existing store tests
also check that a write replaces the connector state, and that a forbidden
write keeps the state and level and drops its runtime PM reference. 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
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
.../amdgpu_dm/tests/amdgpu_dm_backlight_test.c | 157 ++++++++++++++++++++-
1 file changed, 150 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 35e245b078f7a..d58418c88592c 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);
@@ -609,6 +637,21 @@ setup_panel_power_savings_pipe(struct kunit *test, struct device **device_out)
drm_mode_config_reset(&adev->ddev);
+ 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);
@@ -617,6 +660,14 @@ 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_free_sysfs_buf(void *data)
{
free_page((unsigned long)data);
@@ -677,17 +728,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);
}
@@ -695,6 +753,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
+ * drops its runtime PM reference
*/
static void dm_test_panel_power_savings_store_forbidden(struct kunit *test)
{
@@ -702,11 +763,15 @@ 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;
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_no_pm_reference(test, connector);
}
/**
@@ -755,6 +820,81 @@ 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);
}
+/**
+ * dm_test_panel_power_savings_store_inactive_crtc - Test store with the CRTC off
+ * @test: The KUnit test context
+ *
+ * With the connector's CRTC enabled but inactive, the store still 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_inactive_crtc(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, false,
+ &dm_test_pps_crtc_helper_funcs_no_modeset);
+ old_state = connector->state;
+
+ KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "3", 1), 1);
+ KUNIT_EXPECT_PTR_NE(test, connector->state, old_state);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level, 3);
+ 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_active_crtc - Test store with the CRTC on
+ * @test: The KUnit test context
+ *
+ * With the connector's CRTC active, 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_active_crtc(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_no_modeset);
+ old_state = connector->state;
+
+ KUNIT_EXPECT_EQ(test, panel_power_savings_store(device, NULL, "2", 1), 1);
+ KUNIT_EXPECT_PTR_NE(test, connector->state, old_state);
+ KUNIT_EXPECT_EQ(test, to_dm_connector_state(connector->state)->abm_level, 2);
+ 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);
+}
+
/* Tests for amdgpu_dm_backlight_get_device_index() */
/**
@@ -2017,6 +2157,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(dm_test_panel_power_savings_store_inactive_crtc),
+ KUNIT_CASE(dm_test_panel_power_savings_store_active_crtc),
+ KUNIT_CASE(dm_test_panel_power_savings_store_commit_fails),
/* 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 related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-04 14:15 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 10:17 [PATCH 0/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
2026-11-02 10:17 ` [PATCH 1/5] drm/amd/display: Render the panel power savings kernel-doc Hugo Sales
2026-11-02 10:17 ` [PATCH 2/5] drm/amd/display: Don't force a modeset when the ABM level changes Hugo Sales
2026-11-02 10:17 ` [PATCH 3/5] drm/amd/display: Give the panel_power_savings store tests a full pipe Hugo Sales
2026-11-02 10:17 ` [PATCH 4/5] drm/amd/display: Commit panel_power_savings writes from the kernel Hugo Sales
2026-11-02 10:17 ` [PATCH 5/5] drm/amd/display: Test panel_power_savings commits Hugo Sales
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox