dri-devel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/4] drm/amd/display: fix brightness ownership through power module
@ 2026-09-02 12:31 Andrei Rusu de Castro
  2026-09-02 12:31 ` [PATCH 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
                   ` (4 more replies)
  0 siblings, 5 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 12:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

Linux passes PWM eDP brightness through two owners. The display manager
maps the request into the firmware range and applies the ATIF custom
curve. The power module then derives a percentage from that hardware
value and applies the same curve and range again. A non-zero firmware
minimum consequently prevents zero from reaching the panel minimum.
The split also leaves the custom-curve disable policy and final PWM mask
attached to the wrong owner.

Patch 1 keeps pre-power-module custom-curve output in the userspace
domain, which also fixes affected stable kernels. Patch 2 covers that
conversion with a non-zero firmware minimum. Patch 3 passes zero-anchored
millipercent into the power module and moves the panel policy, curve,
range, trace units, and final effective-PWM mask to that owner. Patch 4
covers ordinary PWM, forced PWM, AMD-AUX fallback, true AUX, multiple
panels, endpoints, interior values, clamping, and invalid ranges.

The complete series passed all 85 AMD backlight UML KUnit cases on the
current base and after clean application to Linux 7.3-rc1. It was built
into one kernel, booted on two Strix Halo systems, and produced the same
AMDGPU module on both. An instrumented physical panel test observed:

  request 0     -> 0 millipercent      -> PWM 3084  -> success
  request 65535 -> 100000 millipercent -> PWM 65535 -> success

The panel visibly reached minimum and maximum brightness, then returned
to its starting level. No fatal kernel event followed. The true AUX path
was covered by KUnit but was not available for a physical test. The
existing LUT-unaware hardware readback inverse is unchanged.

Andrei Rusu de Castro (4):
  drm/amd/display: keep custom brightness curve in userspace domain
  drm/amd/display: test custom brightness with non-zero minimum
  drm/amd/display: pass userspace brightness to power module
  drm/amd/display: test power module brightness input domain

 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c |   6 +-
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   |  59 ++-
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |   5 +-
 .../amd/display/amdgpu_dm/amdgpu_dm_trace.h   |   3 +-
 .../tests/amdgpu_dm_backlight_test.c          | 447 +++++++++++++++++-
 .../drm/amd/display/modules/inc/mod_power.h   |   1 +
 .../gpu/drm/amd/display/modules/power/power.c |   2 +
 .../drm/amd/display/modules/power/power_abm.c |   6 +-
 .../amd/display/modules/power/power_helpers.h |   1 +
 9 files changed, 494 insertions(+), 36 deletions(-)



^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH 1/4] drm/amd/display: keep custom brightness curve in userspace domain
  2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
@ 2026-09-02 12:31 ` Andrei Rusu de Castro
  2026-09-02 12:31 ` [PATCH 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 12:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu,
	stable

convert_brightness_from_user() expects convert_custom_brightness() to
reshape a value in the userspace [0..max] domain before the caller maps
it once into the firmware [min..max] domain.

Commit 8dbd72cb7900 ("drm/amd/display: Export full brightness range to
userspace") instead made convert_custom_brightness() call
scale_fw_to_input(min, max, ...). That helper adds min and scales by the
firmware span, entering the firmware domain before the caller applies
the same span and minimum again.

With the default PWM range, min is 3084 and max is 65535. A zero request
through any custom curve consequently returns 6023 instead of 3084, so
the darkest 2939 firmware levels are unreachable.

Keep the curve result in [0..max]. scale_fw_to_input() then becomes the
inverse of scale_input_to_fw(), and the caller remains the sole owner of
the userspace-to-firmware conversion. The linear path is unchanged.

A later patch removes this helper from the PWM power-module path because
the power module owns that curve. AUX panels continue to use it, and
stable kernels predating the power-module refactor still require this
correction on PWM panels.

Commit 6fd83a1c2cde ("drm/amd/display: Scale custom brightness curve from
full range") fixed the corresponding input-side domain error. This
completes the output side of the same conversion.

Source and arithmetic analysis identified the duplicate
domain conversion. KUnit coverage for a non-zero firmware minimum is
added separately.

Fixes: 8dbd72cb7900 ("drm/amd/display: Export full brightness range to userspace")
Cc: stable@vger.kernel.org # 6.17.x: 6fd83a1c2cde: drm/amd/display: Scale custom brightness curve from full range
Cc: stable@vger.kernel.org # 6.17.x
Cc: stable@vger.kernel.org # before 7.3 this code is in amdgpu_dm.c; drop the header and test hunks

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   | 13 ++++----
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |  3 +-
 .../tests/amdgpu_dm_backlight_test.c          | 30 +++++++++----------
 3 files changed, 22 insertions(+), 24 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 e61bbc310f33..424b33573a73 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
@@ -112,16 +112,15 @@ static inline u32 scale_input_to_fw(int max, u64 input)
 	return DIV_ROUND_CLOSEST_ULL(input * AMDGPU_MAX_BL_LEVEL, max);
 }
 
-/* Rescale from [0..AMDGPU_MAX_BL_LEVEL] to [min..max] */
-static inline u32 scale_fw_to_input(int min, int max, u64 input)
+/* Rescale the firmware curve's [0..AMDGPU_MAX_BL_LEVEL] back to userspace [0..max]. */
+static inline u32 scale_fw_to_input(int max, u64 input)
 {
-	return min + DIV_ROUND_CLOSEST_ULL(input * (max - min), AMDGPU_MAX_BL_LEVEL);
+	return DIV_ROUND_CLOSEST_ULL(input * max, AMDGPU_MAX_BL_LEVEL);
 }
 
 STATIC_IFN_KUNIT
 void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
-			       unsigned int min, unsigned int max,
-			       uint32_t *user_brightness)
+			       unsigned int max, uint32_t *user_brightness)
 {
 	u32 brightness = scale_input_to_fw(max, *user_brightness);
 	u8 lower_signal, upper_signal, upper_lum, lower_lum, lum;
@@ -179,7 +178,7 @@ void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
 						    (brightness - lower_signal),
 						    upper_signal - lower_signal);
 scale:
-	*user_brightness = scale_fw_to_input(min, max,
+	*user_brightness = scale_fw_to_input(max,
 					     DIV_ROUND_CLOSEST(lum * brightness, 101));
 }
 
@@ -194,7 +193,7 @@ u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 	if (!get_brightness_range(caps, &min, &max))
 		return brightness;
 
-	convert_custom_brightness(caps, min, max, &brightness);
+	convert_custom_brightness(caps, max, &brightness);
 
 	/* Rescale 0..max to min..max */
 	return min + DIV_ROUND_CLOSEST_ULL((u64)(max - min) * brightness, max);
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
index 07b75064847c..90bed0ea5d00 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
@@ -67,8 +67,7 @@ ssize_t panel_power_savings_store(struct device *device,
 int get_brightness_range(const struct amdgpu_dm_backlight_caps *caps,
 			 unsigned int *min, unsigned int *max);
 void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
-			       unsigned int min, unsigned int max,
-			       uint32_t *user_brightness);
+			       unsigned int max, uint32_t *user_brightness);
 u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 				 uint32_t brightness);
 u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
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 7ca17f803f9d..1fb171fdbc3c 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
@@ -1033,7 +1033,7 @@ static void dm_test_custom_brightness_no_data_points(struct kunit *test)
 
 	caps.data_points = 0;
 
-	convert_custom_brightness(&caps, 3084, 65535, &brightness);
+	convert_custom_brightness(&caps, 65535, &brightness);
 
 	/* No data points → no-op */
 	KUNIT_EXPECT_EQ(test, brightness, saved);
@@ -1057,7 +1057,7 @@ static void dm_test_custom_brightness_debug_mask_disables(struct kunit *test)
 	/* Set the disable flag */
 	amdgpu_dm_set_dc_debug_mask(amdgpu_dm_get_dc_debug_mask() | DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
 
-	convert_custom_brightness(&caps, 3084, 65535, &brightness);
+	convert_custom_brightness(&caps, 65535, &brightness);
 
 	/* Should be no-op due to debug mask */
 	KUNIT_EXPECT_EQ(test, brightness, saved);
@@ -1100,14 +1100,14 @@ static void dm_test_custom_brightness_exact_match(struct kunit *test)
 	 */
 	brightness = 32896;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * Exact match: lum=50, brightness_scaled=128
-	 * result = scale_fw_to_input(min, max, DIV_ROUND_CLOSEST(50*128, 101))
-	 *        = scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(6400, 101))
-	 *        = scale_fw_to_input(0, 65535, 63)
-	 *        = 0 + DIV_ROUND_CLOSEST(63 * 65535, 255) = 16191 (approx)
+	 * result = scale_fw_to_input(max, DIV_ROUND_CLOSEST(50*128, 101))
+	 *        = scale_fw_to_input(65535, DIV_ROUND_CLOSEST(6400, 101))
+	 *        = scale_fw_to_input(65535, 63)
+	 *        = DIV_ROUND_CLOSEST(63 * 65535, 255) = 16191 (approx)
 	 */
 	KUNIT_EXPECT_TRUE(test, brightness != 32896);
 	KUNIT_EXPECT_TRUE(test, brightness < 32896);
@@ -1146,13 +1146,13 @@ static void dm_test_custom_brightness_below_first(struct kunit *test)
 	 */
 	brightness = 12850;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * Below first data point: lum = DIV_ROUND_CLOSEST(40 * 50, 100) = 20
-	 * Then: scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(20 * 50, 101))
-	 *      = scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(1000, 101))
-	 *      = scale_fw_to_input(0, 65535, 10)
+	 * Then: scale_fw_to_input(65535, DIV_ROUND_CLOSEST(20 * 50, 101))
+	 *      = scale_fw_to_input(65535, DIV_ROUND_CLOSEST(1000, 101))
+	 *      = scale_fw_to_input(65535, 10)
 	 * The output should be significantly less than input.
 	 */
 	KUNIT_EXPECT_TRUE(test, brightness < 12850);
@@ -1190,7 +1190,7 @@ static void dm_test_custom_brightness_interpolation(struct kunit *test)
 	 */
 	brightness = 32125;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * The function should interpolate between data points and produce
@@ -1233,7 +1233,7 @@ static void dm_test_custom_brightness_above_last(struct kunit *test)
 	 */
 	brightness = 56533;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/* Output should differ from input (remapped via curve) */
 	KUNIT_EXPECT_TRUE(test, brightness != 56533);
@@ -1271,7 +1271,7 @@ static void dm_test_custom_brightness_single_data_point(struct kunit *test)
 	 */
 	brightness = 16448;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	KUNIT_EXPECT_TRUE(test, brightness < 16448);
 
@@ -1309,7 +1309,7 @@ static void dm_test_custom_brightness_lower_lum_zero(struct kunit *test)
 	 */
 	brightness = 32125;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/* Should remap; result should differ from input */
 	KUNIT_EXPECT_TRUE(test, brightness != 32125);


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH 2/4] drm/amd/display: test custom brightness with non-zero minimum
  2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  2026-09-02 12:31 ` [PATCH 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
@ 2026-09-02 12:31 ` Andrei Rusu de Castro
  2026-09-02 12:32 ` [PATCH 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 12:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

The existing custom-curve tests use min_input_signal == 0. In that
configuration the firmware minimum is zero and an extra [0..max] to
[min..max] conversion is an identity, so the output-side double scaling
cannot be observed.

Add a curve using the default non-zero PWM minimum. Cover the helper's
userspace-domain result, zero and maximum endpoints, interpolation,
monotonicity, range bounds, zero round-trip, agreement with the linear
path at zero, and the unchanged no-curve path.

Six of the nine new cases fail against the pre-fix arithmetic. The other
three retain endpoint and linear-path invariants around the repair.

The cases were verified under UML KUnit.

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../tests/amdgpu_dm_backlight_test.c          | 264 ++++++++++++++++++
 1 file changed, 264 insertions(+)

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 1fb171fdbc3c..a6fa052a8272 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
@@ -1398,6 +1398,261 @@ static void dm_test_brightness_from_user_with_curve(struct kunit *test)
 	amdgpu_dm_set_dc_debug_mask(saved_mask);
 }
 
+/*
+ * The custom curve cases above all use min_input_signal == 0.  There the
+ * firmware minimum is zero and the [0..max] to [min..max] span is the identity,
+ * so an extra application of that mapping cannot be observed.  The cases below
+ * use the default firmware range instead, where min is 0x101 * 12 == 3084 and
+ * max is 0x101 * 255 == 65535.
+ */
+static void dm_test_curve_caps_init(struct amdgpu_dm_backlight_caps *caps)
+{
+	caps->aux_support = false;
+	caps->min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps->max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps->data_points = 3;
+	caps->luminance_data[0].input_signal = 50;
+	caps->luminance_data[0].luminance = 20;
+	caps->luminance_data[1].input_signal = 128;
+	caps->luminance_data[1].luminance = 50;
+	caps->luminance_data[2].input_signal = 255;
+	caps->luminance_data[2].luminance = 100;
+}
+
+/**
+ * dm_test_custom_brightness_user_domain - Curve output is a userspace value
+ * @test: The KUnit test context
+ *
+ * convert_custom_brightness() reshapes a value inside the userspace [0..max]
+ * domain.  Its caller owns the single conversion to the firmware [min..max]
+ * domain, so the firmware minimum must not appear in this result.
+ */
+static void dm_test_custom_brightness_user_domain(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+	u32 brightness;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	/* Zero stays zero; it is not lifted to the firmware minimum here. */
+	brightness = 0;
+	convert_custom_brightness(&caps, max, &brightness);
+	KUNIT_EXPECT_EQ(test, brightness, (u32)0);
+
+	/* The top of the curve stays inside the userspace range. */
+	brightness = max;
+	convert_custom_brightness(&caps, max, &brightness);
+	KUNIT_EXPECT_LE(test, brightness, (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_zero_is_min - Zero maps to the firmware minimum
+ * @test: The KUnit test context
+ *
+ * Zero is the darkest level userspace can ask for and must reach the darkest
+ * level the firmware accepts.
+ */
+static void dm_test_curve_from_user_zero_is_min(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 0), (u32)min);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_matches_linear_at_zero - Curve keeps the lower endpoint
+ * @test: The KUnit test context
+ *
+ * The curve reshapes the interior of the range.  It does not move either
+ * endpoint, so the curved and linear paths must agree at zero.
+ */
+static void dm_test_curve_from_user_matches_linear_at_zero(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	u32 with_curve, without_curve;
+
+	dm_test_curve_caps_init(&caps);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	with_curve = convert_brightness_from_user(&caps, 0);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask | DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	without_curve = convert_brightness_from_user(&caps, 0);
+
+	KUNIT_EXPECT_EQ(test, with_curve, without_curve);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_round_trip_zero - Zero survives a conversion round trip
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_round_trip_zero(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	u32 level;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+
+	level = convert_brightness_from_user(&caps, 0);
+	KUNIT_EXPECT_EQ(test, convert_brightness_to_user(&caps, level), (u32)0);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_interpolation - Interpolated point with a non-zero min
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_interpolation(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+
+	/*
+	 * scale_input_to_fw(65535, 25700) = DIV_ROUND_CLOSEST(25700 * 255, 65535)
+	 * = 100, which falls between the (50, 20) and (128, 50) points:
+	 *   lum = 20 + DIV_ROUND_CLOSEST((50 - 20) * (100 - 50), 128 - 50) = 39
+	 * The curved firmware level is DIV_ROUND_CLOSEST(39 * 100, 101) = 39,
+	 * which is DIV_ROUND_CLOSEST(39 * 65535, 255) = 10023 in the userspace
+	 * domain and 3084 + DIV_ROUND_CLOSEST(62451 * 10023, 65535) = 12635
+	 * once converted to the firmware domain.
+	 */
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 25700), (u32)12635);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_max - The top of the range stays inside the range
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_max(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	/*
+	 * scale_input_to_fw(65535, 65535) = 255 matches the last point exactly,
+	 * so lum = 100 and the curved firmware level is
+	 * DIV_ROUND_CLOSEST(100 * 255, 101) = 252.  That is
+	 * DIV_ROUND_CLOSEST(252 * 65535, 255) = 64764 in the userspace domain
+	 * and 3084 + DIV_ROUND_CLOSEST(62451 * 64764, 65535) = 64800 in the
+	 * firmware domain.
+	 */
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)64800);
+	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_monotonic - A rising curve gives a rising level
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_monotonic(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max, i;
+	u32 previous = 0;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	for (i = 0; i <= max; i += 1023) {
+		u32 level = convert_brightness_from_user(&caps, i);
+
+		KUNIT_ASSERT_GE(test, level, previous);
+		previous = level;
+	}
+
+	KUNIT_EXPECT_GE(test, convert_brightness_from_user(&caps, max), previous);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_within_range - Curved levels never leave [min..max]
+ * @test: The KUnit test context
+ *
+ * The firmware level is programmed through a 16-bit path, so a converted value
+ * above max would wrap and darken the panel.
+ */
+static void dm_test_curve_from_user_within_range(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max, i;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	for (i = 0; i <= max; i += 1023) {
+		u32 level = convert_brightness_from_user(&caps, i);
+
+		KUNIT_ASSERT_GE(test, level, (u32)min);
+		KUNIT_ASSERT_LE(test, level, (u32)max);
+	}
+
+	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_from_user_no_curve_unchanged - The linear path is untouched
+ * @test: The KUnit test context
+ *
+ * Without luminance data the conversion is the plain
+ * min + DIV_ROUND_CLOSEST((max - min) * brightness, max) mapping.
+ */
+static void dm_test_from_user_no_curve_unchanged(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.aux_support = false;
+	caps.min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps.data_points = 0;
+
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 0), (u32)3084);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 16383), (u32)18696);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 32767), (u32)34309);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)65535);
+}
+
 /**
  * dm_test_brightness_range_zero_signals - Test Brightness range with zero min and max signals
  * @test: The KUnit test context
@@ -1955,6 +2210,15 @@ static struct kunit_case dm_backlight_test_cases[] = {
 	KUNIT_CASE(dm_test_brightness_to_user_above_max),
 	KUNIT_CASE(dm_test_brightness_from_user_midrange),
 	KUNIT_CASE(dm_test_brightness_from_user_with_curve),
+	KUNIT_CASE(dm_test_custom_brightness_user_domain),
+	KUNIT_CASE(dm_test_curve_from_user_zero_is_min),
+	KUNIT_CASE(dm_test_curve_from_user_matches_linear_at_zero),
+	KUNIT_CASE(dm_test_curve_from_user_round_trip_zero),
+	KUNIT_CASE(dm_test_curve_from_user_interpolation),
+	KUNIT_CASE(dm_test_curve_from_user_max),
+	KUNIT_CASE(dm_test_curve_from_user_monotonic),
+	KUNIT_CASE(dm_test_curve_from_user_within_range),
+	KUNIT_CASE(dm_test_from_user_no_curve_unchanged),
 	KUNIT_CASE(dm_test_brightness_range_zero_signals),
 	/* amdgpu_dm_backlight_fill_props */
 	KUNIT_CASE(dm_test_backlight_fill_props_ac_linear),


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH 3/4] drm/amd/display: pass userspace brightness to power module
  2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  2026-09-02 12:31 ` [PATCH 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
  2026-09-02 12:31 ` [PATCH 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
@ 2026-09-02 12:32 ` Andrei Rusu de Castro
  2026-09-02 12:57   ` sashiko-bot
  2026-09-02 12:33 ` [PATCH 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  4 siblings, 1 reply; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 12:32 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu,
	stable

The power module consumes millipercent and builds its own PWM lookup
table from the ATIF brightness transfer characteristics.

The Linux display manager instead converts the userspace request to a
firmware level, then derives a percentage from that hardware-domain
value. For a non-zero PWM minimum this maps the minimum above zero and
the maximum above 100 percent. It also applies the ATIF curve once in
the display manager and again in the power module.

Pass the original userspace percentage to the power module on PWM
panels. Keep the existing nits conversion and source-unit brightness
mask for AUX panels.

The brightness mask is a final DP source-level quirk. Carry it into the
power module and apply it when the effective hardware handoff is PWM,
preserving ordinary PWM, forced-PWM OLED, fallback PWM, and mode-change
replay without altering a true AUX handoff.

DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE previously bypassed only the display
manager's copy of the curve. Wire it to the power module's existing
linear bypass and use the selected panel's policy rather than panel
zero's. Label the brightness trace value as millipercent or millinits so
the corrected input domain is explicit.

A live DCN 3.5.1 trace exposed the domain mismatch. KUnit coverage is
added separately.

Fixes: 3c108046e1d6 ("drm/amd/display: Add power module on Linux")
Cc: stable@vger.kernel.org # 7.2.x

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c |  6 ++-
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   | 46 +++++++++++++++----
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |  2 +
 .../amd/display/amdgpu_dm/amdgpu_dm_trace.h   |  3 +-
 .../drm/amd/display/modules/inc/mod_power.h   |  1 +
 .../gpu/drm/amd/display/modules/power/power.c |  2 +
 .../drm/amd/display/modules/power/power_abm.c |  6 ++-
 .../amd/display/modules/power/power_helpers.h |  1 +
 8 files changed, 55 insertions(+), 12 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 ec483276d753..4e730527be4a 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
@@ -425,7 +425,9 @@ static int amdgpu_dm_init_power_module(struct amdgpu_display_manager *dm)
 			!(amdgpu_dc_feature_mask & DC_DISABLE_FRACTIONAL_PWM_MASK);
 		init_data[i].use_custom_backlight_caps = false;
 		init_data[i].custom_backlight_caps_config_no = 0;
-		init_data[i].use_linear_backlight_curve = false;
+		init_data[i].use_linear_backlight_curve =
+			!!(amdgpu_dc_debug_mask &
+			   DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
 		init_data[i].def_varibright_enable = 0;
 		init_data[i].varibright_level = 0;
 		/*
@@ -436,6 +438,8 @@ static int amdgpu_dm_init_power_module(struct amdgpu_display_manager *dm)
 			dm->backlight_caps[i].min_input_signal * 0x101;
 		init_data[i].max_backlight_pwm =
 			dm->backlight_caps[i].max_input_signal * 0x101;
+		init_data[i].brightness_mask =
+			dm->backlight_caps[i].brightness_mask;
 		init_data[i].min_abm_backlight =
 			dm->backlight_caps[i].min_input_signal * 0x101;
 
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 424b33573a73..04e810af4623 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
@@ -218,6 +218,41 @@ u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
 }
 EXPORT_IF_KUNIT(convert_brightness_to_user);
 
+static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_caps *caps,
+					      u32 brightness)
+{
+	unsigned int min, max;
+
+	if (!get_brightness_range(caps, &min, &max) || max <= min)
+		return 0;
+
+	if (brightness >= max)
+		return 100 * 1000;
+
+	return DIV_ROUND_CLOSEST_ULL((u64)brightness * 100 * 1000, max);
+}
+
+STATIC_IFN_KUNIT
+u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
+					u32 user_brightness)
+{
+	u32 brightness;
+
+	if (!caps)
+		return user_brightness;
+
+	if (!caps->aux_support)
+		return convert_brightness_to_millipercent(caps, user_brightness);
+
+	brightness = convert_brightness_from_user(caps, user_brightness);
+	if (caps->brightness_mask)
+		brightness |= caps->brightness_mask;
+
+	return brightness;
+}
+
+EXPORT_IF_KUNIT(convert_brightness_for_power_module);
+
 STATIC_IFN_KUNIT
 struct dc_stream_state *dm_find_stream_with_link(
 	struct amdgpu_display_manager *dm,
@@ -262,7 +297,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 	bool rc = false, reallow_idle = false;
 	struct drm_connector *connector;
 	struct dc_stream_state *stream;
-	unsigned int min, max;
 
 	list_for_each_entry(connector, &dm->ddev->mode_config.connector_list, head) {
 		struct amdgpu_dm_connector *aconnector = to_amdgpu_dm_connector(connector);
@@ -285,12 +319,9 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 	/* update scratch register */
 	if (bl_idx == 0)
 		amdgpu_atombios_scratch_regs_set_backlight_level(dm->adev, dm->brightness[bl_idx]);
-	brightness = convert_brightness_from_user(caps, dm->brightness[bl_idx]);
 	link = (struct dc_link *)dm->backlight_link[bl_idx];
-
-	/* Apply brightness quirk */
-	if (caps->brightness_mask)
-		brightness |= caps->brightness_mask;
+	brightness = convert_brightness_for_power_module(caps,
+							 dm->brightness[bl_idx]);
 
 	if (trace_amdgpu_dm_brightness_enabled()) {
 		trace_amdgpu_dm_brightness(__builtin_return_address(0),
@@ -314,9 +345,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 		rc = mod_power_set_backlight_nits(dm->power_module, stream, brightness,
 			AUX_BL_DEFAULT_TRANSITION_TIME_MS, false, true);
 	} else {
-		/* power module uses millipercent */
-		get_brightness_range(caps, &min, &max);
-		brightness = DIV_ROUND_CLOSEST(brightness * 100, (max - min)) * 1000;
 		rc = mod_power_set_backlight_percent(dm->power_module, stream,
 						     brightness, 0, false);
 	}
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
index 90bed0ea5d00..396e7654e299 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
@@ -72,6 +72,8 @@ u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 				 uint32_t brightness);
 u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
 			       uint32_t brightness);
+u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
+					u32 user_brightness);
 int amdgpu_dm_backlight_get_device_index(struct amdgpu_display_manager *dm,
 					 struct backlight_device *bd);
 void amdgpu_dm_backlight_fill_props(const struct amdgpu_dm_backlight_caps *caps,
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
index f33a2c1e0da5..5e7782f9e89f 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
@@ -744,10 +744,11 @@ TRACE_EVENT(amdgpu_dm_brightness,
 		__entry->aux = aux;
 		__entry->ac = ac;
 	),
-	TP_printk("%ps: brightness requested=%u converted=%u aux=%s power=%s",
+	TP_printk("%ps: brightness requested=%u converted=%u unit=%s aux=%s power=%s",
 		  (void *)__entry->function,
 		  (u32)__entry->user_brightness,
 		  (u32)__entry->converted_brightness,
+		  (__entry->aux) ? "millinits" : "millipercent",
 		  (__entry->aux) ? "true" : "false",
 		  (__entry->ac) ? "AC" : "DC"
 	)
diff --git a/drivers/gpu/drm/amd/display/modules/inc/mod_power.h b/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
index 02bee3b1956d..56b41861e164 100644
--- a/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
+++ b/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
@@ -19,6 +19,7 @@ struct mod_power_init_params {
 
 	unsigned int min_backlight_pwm;
 	unsigned int max_backlight_pwm;
+	unsigned int brightness_mask;
 
 	unsigned int min_abm_backlight;
 	unsigned int num_backlight_levels;
diff --git a/drivers/gpu/drm/amd/display/modules/power/power.c b/drivers/gpu/drm/amd/display/modules/power/power.c
index ee15c14a899e..1d5e94893e36 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power.c
+++ b/drivers/gpu/drm/amd/display/modules/power/power.c
@@ -157,6 +157,8 @@ struct mod_power *mod_power_create(struct dc *dc,
 				init_params[inst].use_custom_backlight_caps;
 		core_power->bl_prop[inst].custom_backlight_caps_config_no =
 				init_params[inst].custom_backlight_caps_config_no;
+		core_power->bl_prop[inst].brightness_mask =
+				init_params[inst].brightness_mask;
 
 		// Do not allow less than 101 backlight levels
 		if (init_params[inst].num_backlight_levels < 101)
diff --git a/drivers/gpu/drm/amd/display/modules/power/power_abm.c b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
index 5e86889eaa84..4d7d80ac60c9 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power_abm.c
+++ b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
@@ -529,7 +529,7 @@ static unsigned int backlight_millipercent_to_pwm_legacy(
 		return 0;
 
 	// Bypass the brightness mapping LUT
-	if (core_power->bl_prop->use_linear_backlight_curve) {
+	if (core_power->bl_prop[inst].use_linear_backlight_curve) {
 		pwm = core_power->bl_prop[inst].min_backlight_pwm +
 			(unsigned int) div_u64((unsigned long long) millipercent *
 			core_power->bl_prop[inst].backlight_range,
@@ -1020,6 +1020,10 @@ void fill_backlight_level_params(struct core_power *core_power,
 
 	if (backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX && !is_hdr)
 		backlight_level_params->control_type = BACKLIGHT_CONTROL_PWM;
+
+	if (backlight_level_params->control_type == BACKLIGHT_CONTROL_PWM)
+		backlight_level_params->backlight_pwm_u16_16 |=
+			bl_prop->brightness_mask;
 }
 
 bool mod_power_set_backlight_nits(struct mod_power *mod_power,
diff --git a/drivers/gpu/drm/amd/display/modules/power/power_helpers.h b/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
index 68679fa10946..1eeacbfb8d91 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
+++ b/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
@@ -94,6 +94,7 @@ struct pwr_backlight_properties {
 	unsigned int min_backlight_pwm;
 	unsigned int max_backlight_pwm;
 	unsigned int backlight_range;
+	unsigned int brightness_mask;
 
 	/* Describes the panel's min and max luminance in millinits measured
 	 * on full white screen, in min and max backlight settings.


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH 4/4] drm/amd/display: test power module brightness input domain
  2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
                   ` (2 preceding siblings ...)
  2026-09-02 12:32 ` [PATCH 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
@ 2026-09-02 12:33 ` Andrei Rusu de Castro
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  4 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 12:33 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

Exercise the exact value selected for the power module on PWM and AUX
panels. Cover non-zero and zero PWM minimums, both endpoints, an
interior value, clamping, invalid caps, and retention of the AUX custom
curve and source-unit brightness mask.

The PWM case carries valid ATIF points to prove that the display manager
passes the userspace percentage without applying that curve before the
power module applies its own lookup table. Additional cases prove that
the brightness mask follows the effective control type: true AUX remains
unchanged, while ordinary PWM and AUX fallback-to-PWM are masked. The
selected panel's linear-curve bypass is covered independently.

The cases were verified under UML KUnit.

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../tests/amdgpu_dm_backlight_test.c          | 159 +++++++++++++++++-
 1 file changed, 156 insertions(+), 3 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 a6fa052a8272..e0d46635130b 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
@@ -23,6 +23,7 @@
 #include "amdgpu_dm_kunit_test_helpers.h"
 #include "amd_shared.h"
 #include "link_service.h"
+#include "modules/power/power_helpers.h"
 #include "dc/inc/hw/panel_cntl.h"
 
 struct dm_backlight_connector_fixture {
@@ -1019,6 +1020,151 @@ static void dm_test_brightness_from_user_aux(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)max);
 }
 
+/* Tests for convert_brightness_for_power_module() */
+
+/**
+ * dm_test_power_module_brightness_invalid_caps - Test invalid PWM range
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_brightness_invalid_caps(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(NULL, 100), 100U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 100), 0U);
+}
+
+/**
+ * dm_test_power_module_pwm_uses_user_domain - Test PWM input domain
+ * @test: The KUnit test context
+ *
+ * The power module owns the ATIF luminance-to-PWM curve. The display manager
+ * must therefore pass the original userspace percentage rather than first
+ * converting it to a firmware PWM level.
+ */
+static void dm_test_power_module_pwm_uses_user_domain(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps.data_points = 3;
+	caps.luminance_data[0].input_signal = 50;
+	caps.luminance_data[0].luminance = 20;
+	caps.luminance_data[1].input_signal = 128;
+	caps.luminance_data[1].luminance = 50;
+	caps.luminance_data[2].input_signal = 230;
+	caps.luminance_data[2].luminance = 90;
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, min, 3084U);
+	KUNIT_EXPECT_EQ(test, max, 65535U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 0), 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 25700), 39216U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 32768), 50001U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max), 100000U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max + 1), 100000U);
+}
+
+/**
+ * dm_test_power_module_pwm_zero_min - Test zero-minimum range
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_pwm_zero_min(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.min_input_signal = 0;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, min, 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 0), 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max), 100000U);
+}
+
+/**
+ * dm_test_power_module_mask_follows_effective_control - Test mask handoff
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_mask_follows_effective_control(struct kunit *test)
+{
+	struct set_backlight_level_params params = {};
+	struct core_power core_power = {};
+
+	core_power.bl_prop[0].brightness_mask = 3;
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3087U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 32896,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 32899U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 65535,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 65535U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_AMD_AUX, 0, 0, true);
+	KUNIT_EXPECT_EQ(test, params.control_type, BACKLIGHT_CONTROL_AMD_AUX);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3084U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_AMD_AUX, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.control_type, BACKLIGHT_CONTROL_PWM);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3087U);
+}
+
+/**
+ * dm_test_power_module_linear_curve_uses_panel_instance - Test linear bypass
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_linear_curve_uses_panel_instance(struct kunit *test)
+{
+	struct core_power core_power = {};
+	unsigned int backlight_lut[101] = {};
+
+	core_power.bl_prop[0].backlight_lut = backlight_lut;
+	core_power.bl_prop[0].num_backlight_levels = ARRAY_SIZE(backlight_lut);
+	core_power.bl_prop[1].min_backlight_pwm = 3084;
+	core_power.bl_prop[1].max_backlight_pwm = 65535;
+	core_power.bl_prop[1].backlight_range = 62451;
+	core_power.bl_prop[1].use_linear_backlight_curve = true;
+	backlight_lut[50] = 12345;
+
+	KUNIT_EXPECT_EQ(test, backlight_millipercent_to_pwm(&core_power, 50000, 1),
+			34309U);
+}
+
+/**
+ * dm_test_power_module_aux_keeps_curve_and_mask - Test AUX conversion path
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_aux_keeps_curve_and_mask(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	caps.aux_support = true;
+	caps.aux_min_input_signal = 1;
+	caps.aux_max_input_signal = 512;
+	caps.brightness_mask = 3;
+	caps.data_points = 2;
+	caps.luminance_data[0].input_signal = 50;
+	caps.luminance_data[0].luminance = 20;
+	caps.luminance_data[1].input_signal = 200;
+	caps.luminance_data[1].luminance = 80;
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 200000),
+			81159U);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
 /* Tests for convert_custom_brightness() */
 
 /**
@@ -1590,7 +1736,7 @@ static void dm_test_curve_from_user_monotonic(struct kunit *test)
 	for (i = 0; i <= max; i += 1023) {
 		u32 level = convert_brightness_from_user(&caps, i);
 
-		KUNIT_ASSERT_GE(test, level, previous);
+		KUNIT_EXPECT_GE(test, level, previous);
 		previous = level;
 	}
 
@@ -1619,8 +1765,8 @@ static void dm_test_curve_from_user_within_range(struct kunit *test)
 	for (i = 0; i <= max; i += 1023) {
 		u32 level = convert_brightness_from_user(&caps, i);
 
-		KUNIT_ASSERT_GE(test, level, (u32)min);
-		KUNIT_ASSERT_LE(test, level, (u32)max);
+		KUNIT_EXPECT_GE(test, level, (u32)min);
+		KUNIT_EXPECT_LE(test, level, (u32)max);
 	}
 
 	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
@@ -2198,6 +2344,13 @@ static struct kunit_case dm_backlight_test_cases[] = {
 	KUNIT_CASE(dm_test_brightness_from_user_zero),
 	KUNIT_CASE(dm_test_brightness_from_user_max),
 	KUNIT_CASE(dm_test_brightness_from_user_aux),
+	/* convert_brightness_for_power_module */
+	KUNIT_CASE(dm_test_power_module_brightness_invalid_caps),
+	KUNIT_CASE(dm_test_power_module_pwm_uses_user_domain),
+	KUNIT_CASE(dm_test_power_module_pwm_zero_min),
+	KUNIT_CASE(dm_test_power_module_mask_follows_effective_control),
+	KUNIT_CASE(dm_test_power_module_linear_curve_uses_panel_instance),
+	KUNIT_CASE(dm_test_power_module_aux_keeps_curve_and_mask),
 	/* convert_custom_brightness */
 	KUNIT_CASE(dm_test_custom_brightness_no_data_points),
 	KUNIT_CASE(dm_test_custom_brightness_debug_mask_disables),


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH 3/4] drm/amd/display: pass userspace brightness to power module
  2026-09-02 12:32 ` [PATCH 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
@ 2026-09-02 12:57   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-09-02 12:57 UTC (permalink / raw)
  To: Andrei Rusu de Castro; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

New issues:
- [High] Double application of PWM bitmask quirk and mathematical corruption on millinits.

Pre-existing issues:
- [High] Sleeping function called from atomic context.
- [High] Potential division by zero in brightness conversion functions when processing malformed firmware capabilities.
--

commit 5d73b2399ca1fd59d638eb586a83e6e63cc7a221
Author: Andrei Rusu de Castro <arc@empyreal.works>

drm/amd/display: pass userspace brightness to power module

The power module consumes millipercent and builds its own PWM lookup table
from the ATIF brightness transfer characteristics. This patch passes the
original userspace percentage to the power module on PWM panels and keeps
the existing nits conversion and source-unit brightness mask for AUX panels.
It also wires the brightness bypass to the power module's existing linear
bypass.

> 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 424b33573a734..04e810af4623c 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
> @@ -218,6 +218,41 @@ u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
>  }
>  EXPORT_IF_KUNIT(convert_brightness_to_user);

[Severity: High]
This isn't a bug introduced by this patch, but can
convert_brightness_to_user() in
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c trigger a
kernel panic via division by zero?

If a connected monitor's EDID/DPCD reports identical max_luminance and
min_luminance values:
amdgpu_dm_update_backlight_caps() will set
aux_max_input_signal == aux_min_input_signal.

This causes max - min == 0 to be returned by get_brightness_range().
Subsequently, calling amdgpu_dm_backlight_get_level() will execute
convert_brightness_to_user(), which evaluates
DIV_ROUND_CLOSEST_ULL(..., max - min), triggering a division by zero.

This could allow a Denial of Service attack against the host system by
plugging in an untrusted, maliciously crafted DisplayPort monitor or dock.

> +static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_caps *caps,
> +					      u32 brightness)
[ ... ]
> +STATIC_IFN_KUNIT
> +u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
> +					u32 user_brightness)
> +{
> +	u32 brightness;
> +
> +	if (!caps)
> +		return user_brightness;
> +
> +	if (!caps->aux_support)
> +		return convert_brightness_to_millipercent(caps, user_brightness);
> +
> +	brightness = convert_brightness_from_user(caps, user_brightness);
> +	if (caps->brightness_mask)
> +		brightness |= caps->brightness_mask;

[Severity: High]
Does convert_brightness_for_power_module() in
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c erroneously
apply the hardware bitmask quirk to a mathematical millinits value prior to
handoff?

For AUX panels, caps->brightness_mask is OR'ed into the mathematically
scaled millinits brightness variable here. This corrupted value is then
passed to mod_power_set_backlight_nits(), which converts it to a PWM value.

> +
> +	return brightness;
> +}
> +
> +EXPORT_IF_KUNIT(convert_brightness_for_power_module);
> +
>  STATIC_IFN_KUNIT
>  struct dc_stream_state *dm_find_stream_with_link(
>  	struct amdgpu_display_manager *dm,
> @@ -262,7 +297,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
>  	bool rc = false, reallow_idle = false;
>  	struct drm_connector *connector;
>  	struct dc_stream_state *stream;

[Severity: High]
This isn't a bug introduced by this patch, but does
amdgpu_dm_backlight_set_level() in
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c safely handle
calling mutex_lock(&dm->dc_lock)?

amdgpu_dm_backlight_set_level() explicitly locks the mutex, but it is
reached via amdgpu_dm_atomic_commit_tail() during non-blocking display
atomic commits. Calling a sleeping function from atomic context can lead to
kernel warnings (lockdep splats), system instability, and potential
deadlocks.

>  
>  	list_for_each_entry(connector, &dm->ddev->mode_config.connector_list, head) {
>  		struct amdgpu_dm_connector *aconnector = to_amdgpu_dm_connector(connector);
[ ... ]
> diff --git a/drivers/gpu/drm/amd/display/modules/power/power_abm.c b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
> index 5e86889eaa841..4d7d80ac60c9b 100644
> --- a/drivers/gpu/drm/amd/display/modules/power/power_abm.c
> +++ b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
> @@ -1020,6 +1020,10 @@ void fill_backlight_level_params(struct core_power *core_power,
>  
>  	if (backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX && !is_hdr)
>  		backlight_level_params->control_type = BACKLIGHT_CONTROL_PWM;
> +
> +	if (backlight_level_params->control_type == BACKLIGHT_CONTROL_PWM)
> +		backlight_level_params->backlight_pwm_u16_16 |=
> +			bl_prop->brightness_mask;

[Severity: High]
For non-HDR AUX panels falling back to PWM, does
fill_backlight_level_params() in
drivers/gpu/drm/amd/display/modules/power/power_abm.c apply the bitmask
a second time onto the resulting PWM value, leading to undefined hardware
behavior?

>  }
>  
>  bool mod_power_set_backlight_nits(struct mod_power *mod_power,

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260902-brightness-cover-abf809f2@empyreal.works?part=3

^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module
  2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
                   ` (3 preceding siblings ...)
  2026-09-02 12:33 ` [PATCH 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro
@ 2026-09-02 22:31 ` Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
                     ` (3 more replies)
  4 siblings, 4 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 22:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

Linux passes PWM eDP brightness through two owners. The display manager
maps the request into the firmware range and applies the ATIF custom
curve. The power module then derives a percentage from that hardware
value and applies the same curve and range again. A non-zero firmware
minimum consequently prevents zero from reaching the panel minimum.
The split also leaves the custom-curve disable policy and final source
brightness mask attached to the wrong owner.

Patch 1 keeps pre-power-module custom-curve output in the userspace
domain, which also fixes affected stable kernels. Patch 2 covers that
conversion with a non-zero firmware minimum. Patch 3 passes zero-anchored
millipercent into the power module and keeps AUX millinits unmasked until
the final AMD AUX or effective PWM handoff. Patch 4 covers ordinary PWM,
forced PWM, AMD-AUX fallback, true AUX, live and replay callbacks,
multiple panels, endpoints, interior values, clamping, and invalid
ranges.

GPT-5.6 Sol Fast assisted with issue investigation, source and history
analysis, patch and test development, review-response analysis,
submission preparation, and consolidation of the working context. A
separate tool-enabled Claude Opus 5 review audited the complete final
diff with direct access to the source tree, build environment, KUnit
harness, logs, and relevant hardware records. Objective verification
included strict checkpatch, a clean UML KUnit build and execution of the
configured DRM test set, an exhaustive C reproduction of every EDID
luminance-byte combination, and source tracing through the DRM commit
worker and hardware callbacks.

The human contributor reviewed the final behavior and evidence, directly
performed and observed the physical hardware QA, and explicitly approved
the exact tested series for submission. The quirked OLED path, VESA AUX
panel, pre-DCN3.1 hardware, and hardware mode-change replay on a quirked
panel remain untested physically because no matching panel is available.

Changes in v2:

- rebase onto current Linux master 89a312991dc6;
- retain mathematical AUX millinits until the hardware handoff;
- apply the panel mask immediately before live and replay AMD AUX writes,
  or after PWM derivation when the effective handoff is PWM;
- add a composition regression whose input distinguishes intermediate
  masking from final-only masking;
- capture and verify the live AMD AUX, VESA AUX, and mode-change callback
  values;
- credit the Sashiko finding and add the documented LLM assistance
  trailer.

Sashiko also reported two pre-existing potential issues. Both were
checked without changing this series. The DRM nonblocking commit tail
runs from system_dfl_wq in sleepable process context, so taking dc_lock
there is valid. The EDID parser and AMD fallback were exhausted across
all 65,536 max_fall/min_cll byte combinations: consumed AUX ranges were
always min 1 and max 50 through 12544, with no equal or inverted range.

The final candidate passed all 2,036 configured UML KUnit tests,
including 88 AMD backlight cases. Each patch passes strict checkpatch
with zero errors, warnings, or checks, excluding only commit lookup
diagnostics caused by the shallow source checkout. The production AMD
display objects also compile with KUnit disabled and W=1.

The v2 physical PWM sweep on a Strix Halo panel captured both hardware
endpoints and restoration:

  requested 655   -> actual 0
  requested 65535 -> actual 65535
  restored requested 20119 -> actual 11597

The operator reported normal panel behavior through the sweep. No kernel
fault followed. This panel has brightness_mask == 0, so it validates the
shared path but not the quirked OLED hardware path. The existing
LUT-unaware hardware readback inverse remains unchanged.

Andrei Rusu de Castro (4):
  drm/amd/display: keep custom brightness curve in userspace domain
  drm/amd/display: test custom brightness with non-zero minimum
  drm/amd/display: pass userspace brightness to power module
  drm/amd/display: test power module brightness input domain

 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c |   6 +-
 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h |   2 +-
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   |  57 +-
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |   5 +-
 .../amd/display/amdgpu_dm/amdgpu_dm_trace.h   |   3 +-
 .../tests/amdgpu_dm_backlight_test.c          | 622 +++++++++++++++++-
 .../drm/amd/display/modules/inc/mod_power.h   |   1 +
 .../gpu/drm/amd/display/modules/power/power.c |   2 +
 .../drm/amd/display/modules/power/power_abm.c |  55 +-
 .../amd/display/modules/power/power_helpers.h |  14 +
 10 files changed, 721 insertions(+), 46 deletions(-)


base-commit: 89a312991dc6e638a36adc43ccb91dbc25504c04
-- 
2.54.0



^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH v2 1/4] drm/amd/display: keep custom brightness curve in userspace domain
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
@ 2026-09-02 22:31   ` Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 22:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu,
	stable

convert_brightness_from_user() expects convert_custom_brightness() to
reshape a value in the userspace [0..max] domain before the caller maps
it once into the firmware [min..max] domain.

Commit 8dbd72cb7900 ("drm/amd/display: Export full brightness range to
userspace") instead made convert_custom_brightness() call
scale_fw_to_input(min, max, ...). That helper adds min and scales by the
firmware span, entering the firmware domain before the caller applies
the same span and minimum again.

With the default PWM range, min is 3084 and max is 65535. A zero request
through any custom curve consequently returns 6023 instead of 3084, so
the darkest 2939 firmware levels are unreachable.

Keep the curve result in [0..max]. scale_fw_to_input() then becomes the
inverse of scale_input_to_fw(), and the caller remains the sole owner of
the userspace-to-firmware conversion. The linear path is unchanged.

A later patch removes this helper from the PWM power-module path because
the power module owns that curve. AUX panels continue to use it, and
stable kernels predating the power-module refactor still require this
correction on PWM panels.

Commit 6fd83a1c2cde ("drm/amd/display: Scale custom brightness curve from
full range") fixed the corresponding input-side domain error. This
completes the output side of the same conversion.

Source and arithmetic analysis identified the duplicate domain
conversion. KUnit coverage for a non-zero firmware minimum is added
separately.

Fixes: 8dbd72cb7900 ("drm/amd/display: Export full brightness range to userspace")
Cc: stable@vger.kernel.org # 6.17.x: 6fd83a1c2cde: drm/amd/display: Scale custom brightness curve from full range
Cc: stable@vger.kernel.org # 6.17.x
Cc: stable@vger.kernel.org # before 7.3 this code is in amdgpu_dm.c; drop the header and test hunks
Assisted-by: LLM
Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   | 13 ++++----
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |  3 +-
 .../tests/amdgpu_dm_backlight_test.c          | 30 +++++++++----------
 3 files changed, 22 insertions(+), 24 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 e61bbc310f33..424b33573a73 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
@@ -112,16 +112,15 @@ static inline u32 scale_input_to_fw(int max, u64 input)
 	return DIV_ROUND_CLOSEST_ULL(input * AMDGPU_MAX_BL_LEVEL, max);
 }
 
-/* Rescale from [0..AMDGPU_MAX_BL_LEVEL] to [min..max] */
-static inline u32 scale_fw_to_input(int min, int max, u64 input)
+/* Rescale the firmware curve's [0..AMDGPU_MAX_BL_LEVEL] back to userspace [0..max]. */
+static inline u32 scale_fw_to_input(int max, u64 input)
 {
-	return min + DIV_ROUND_CLOSEST_ULL(input * (max - min), AMDGPU_MAX_BL_LEVEL);
+	return DIV_ROUND_CLOSEST_ULL(input * max, AMDGPU_MAX_BL_LEVEL);
 }
 
 STATIC_IFN_KUNIT
 void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
-			       unsigned int min, unsigned int max,
-			       uint32_t *user_brightness)
+			       unsigned int max, uint32_t *user_brightness)
 {
 	u32 brightness = scale_input_to_fw(max, *user_brightness);
 	u8 lower_signal, upper_signal, upper_lum, lower_lum, lum;
@@ -179,7 +178,7 @@ void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
 						    (brightness - lower_signal),
 						    upper_signal - lower_signal);
 scale:
-	*user_brightness = scale_fw_to_input(min, max,
+	*user_brightness = scale_fw_to_input(max,
 					     DIV_ROUND_CLOSEST(lum * brightness, 101));
 }
 
@@ -194,7 +193,7 @@ u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 	if (!get_brightness_range(caps, &min, &max))
 		return brightness;
 
-	convert_custom_brightness(caps, min, max, &brightness);
+	convert_custom_brightness(caps, max, &brightness);
 
 	/* Rescale 0..max to min..max */
 	return min + DIV_ROUND_CLOSEST_ULL((u64)(max - min) * brightness, max);
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
index 07b75064847c..90bed0ea5d00 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
@@ -67,8 +67,7 @@ ssize_t panel_power_savings_store(struct device *device,
 int get_brightness_range(const struct amdgpu_dm_backlight_caps *caps,
 			 unsigned int *min, unsigned int *max);
 void convert_custom_brightness(const struct amdgpu_dm_backlight_caps *caps,
-			       unsigned int min, unsigned int max,
-			       uint32_t *user_brightness);
+			       unsigned int max, uint32_t *user_brightness);
 u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 				 uint32_t brightness);
 u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
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 7ca17f803f9d..1fb171fdbc3c 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
@@ -1033,7 +1033,7 @@ static void dm_test_custom_brightness_no_data_points(struct kunit *test)
 
 	caps.data_points = 0;
 
-	convert_custom_brightness(&caps, 3084, 65535, &brightness);
+	convert_custom_brightness(&caps, 65535, &brightness);
 
 	/* No data points → no-op */
 	KUNIT_EXPECT_EQ(test, brightness, saved);
@@ -1057,7 +1057,7 @@ static void dm_test_custom_brightness_debug_mask_disables(struct kunit *test)
 	/* Set the disable flag */
 	amdgpu_dm_set_dc_debug_mask(amdgpu_dm_get_dc_debug_mask() | DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
 
-	convert_custom_brightness(&caps, 3084, 65535, &brightness);
+	convert_custom_brightness(&caps, 65535, &brightness);
 
 	/* Should be no-op due to debug mask */
 	KUNIT_EXPECT_EQ(test, brightness, saved);
@@ -1100,14 +1100,14 @@ static void dm_test_custom_brightness_exact_match(struct kunit *test)
 	 */
 	brightness = 32896;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * Exact match: lum=50, brightness_scaled=128
-	 * result = scale_fw_to_input(min, max, DIV_ROUND_CLOSEST(50*128, 101))
-	 *        = scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(6400, 101))
-	 *        = scale_fw_to_input(0, 65535, 63)
-	 *        = 0 + DIV_ROUND_CLOSEST(63 * 65535, 255) = 16191 (approx)
+	 * result = scale_fw_to_input(max, DIV_ROUND_CLOSEST(50*128, 101))
+	 *        = scale_fw_to_input(65535, DIV_ROUND_CLOSEST(6400, 101))
+	 *        = scale_fw_to_input(65535, 63)
+	 *        = DIV_ROUND_CLOSEST(63 * 65535, 255) = 16191 (approx)
 	 */
 	KUNIT_EXPECT_TRUE(test, brightness != 32896);
 	KUNIT_EXPECT_TRUE(test, brightness < 32896);
@@ -1146,13 +1146,13 @@ static void dm_test_custom_brightness_below_first(struct kunit *test)
 	 */
 	brightness = 12850;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * Below first data point: lum = DIV_ROUND_CLOSEST(40 * 50, 100) = 20
-	 * Then: scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(20 * 50, 101))
-	 *      = scale_fw_to_input(0, 65535, DIV_ROUND_CLOSEST(1000, 101))
-	 *      = scale_fw_to_input(0, 65535, 10)
+	 * Then: scale_fw_to_input(65535, DIV_ROUND_CLOSEST(20 * 50, 101))
+	 *      = scale_fw_to_input(65535, DIV_ROUND_CLOSEST(1000, 101))
+	 *      = scale_fw_to_input(65535, 10)
 	 * The output should be significantly less than input.
 	 */
 	KUNIT_EXPECT_TRUE(test, brightness < 12850);
@@ -1190,7 +1190,7 @@ static void dm_test_custom_brightness_interpolation(struct kunit *test)
 	 */
 	brightness = 32125;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/*
 	 * The function should interpolate between data points and produce
@@ -1233,7 +1233,7 @@ static void dm_test_custom_brightness_above_last(struct kunit *test)
 	 */
 	brightness = 56533;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/* Output should differ from input (remapped via curve) */
 	KUNIT_EXPECT_TRUE(test, brightness != 56533);
@@ -1271,7 +1271,7 @@ static void dm_test_custom_brightness_single_data_point(struct kunit *test)
 	 */
 	brightness = 16448;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	KUNIT_EXPECT_TRUE(test, brightness < 16448);
 
@@ -1309,7 +1309,7 @@ static void dm_test_custom_brightness_lower_lum_zero(struct kunit *test)
 	 */
 	brightness = 32125;
 
-	convert_custom_brightness(&caps, min, max, &brightness);
+	convert_custom_brightness(&caps, max, &brightness);
 
 	/* Should remap; result should differ from input */
 	KUNIT_EXPECT_TRUE(test, brightness != 32125);
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 2/4] drm/amd/display: test custom brightness with non-zero minimum
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
@ 2026-09-02 22:31   ` Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
  2026-09-02 22:32   ` [PATCH v2 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro
  3 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 22:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

The existing custom-curve tests use min_input_signal == 0. In that
configuration the firmware minimum is zero and an extra [0..max] to
[min..max] conversion is an identity, so the output-side double scaling
cannot be observed.

Add a curve using the default non-zero PWM minimum. Cover the helper's
userspace-domain result, zero and maximum endpoints, interpolation,
monotonicity, range bounds, zero round-trip, agreement with the linear
path at zero, and the unchanged no-curve path.

Six of the nine new cases fail against the pre-fix arithmetic. The other
three retain endpoint and linear-path invariants around the repair.

The cases were verified under UML KUnit.

Assisted-by: LLM
Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../tests/amdgpu_dm_backlight_test.c          | 264 ++++++++++++++++++
 1 file changed, 264 insertions(+)

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 1fb171fdbc3c..a6fa052a8272 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
@@ -1398,6 +1398,261 @@ static void dm_test_brightness_from_user_with_curve(struct kunit *test)
 	amdgpu_dm_set_dc_debug_mask(saved_mask);
 }
 
+/*
+ * The custom curve cases above all use min_input_signal == 0.  There the
+ * firmware minimum is zero and the [0..max] to [min..max] span is the identity,
+ * so an extra application of that mapping cannot be observed.  The cases below
+ * use the default firmware range instead, where min is 0x101 * 12 == 3084 and
+ * max is 0x101 * 255 == 65535.
+ */
+static void dm_test_curve_caps_init(struct amdgpu_dm_backlight_caps *caps)
+{
+	caps->aux_support = false;
+	caps->min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps->max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps->data_points = 3;
+	caps->luminance_data[0].input_signal = 50;
+	caps->luminance_data[0].luminance = 20;
+	caps->luminance_data[1].input_signal = 128;
+	caps->luminance_data[1].luminance = 50;
+	caps->luminance_data[2].input_signal = 255;
+	caps->luminance_data[2].luminance = 100;
+}
+
+/**
+ * dm_test_custom_brightness_user_domain - Curve output is a userspace value
+ * @test: The KUnit test context
+ *
+ * convert_custom_brightness() reshapes a value inside the userspace [0..max]
+ * domain.  Its caller owns the single conversion to the firmware [min..max]
+ * domain, so the firmware minimum must not appear in this result.
+ */
+static void dm_test_custom_brightness_user_domain(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+	u32 brightness;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	/* Zero stays zero; it is not lifted to the firmware minimum here. */
+	brightness = 0;
+	convert_custom_brightness(&caps, max, &brightness);
+	KUNIT_EXPECT_EQ(test, brightness, (u32)0);
+
+	/* The top of the curve stays inside the userspace range. */
+	brightness = max;
+	convert_custom_brightness(&caps, max, &brightness);
+	KUNIT_EXPECT_LE(test, brightness, (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_zero_is_min - Zero maps to the firmware minimum
+ * @test: The KUnit test context
+ *
+ * Zero is the darkest level userspace can ask for and must reach the darkest
+ * level the firmware accepts.
+ */
+static void dm_test_curve_from_user_zero_is_min(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 0), (u32)min);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_matches_linear_at_zero - Curve keeps the lower endpoint
+ * @test: The KUnit test context
+ *
+ * The curve reshapes the interior of the range.  It does not move either
+ * endpoint, so the curved and linear paths must agree at zero.
+ */
+static void dm_test_curve_from_user_matches_linear_at_zero(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	u32 with_curve, without_curve;
+
+	dm_test_curve_caps_init(&caps);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	with_curve = convert_brightness_from_user(&caps, 0);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask | DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	without_curve = convert_brightness_from_user(&caps, 0);
+
+	KUNIT_EXPECT_EQ(test, with_curve, without_curve);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_round_trip_zero - Zero survives a conversion round trip
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_round_trip_zero(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	u32 level;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+
+	level = convert_brightness_from_user(&caps, 0);
+	KUNIT_EXPECT_EQ(test, convert_brightness_to_user(&caps, level), (u32)0);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_interpolation - Interpolated point with a non-zero min
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_interpolation(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+
+	/*
+	 * scale_input_to_fw(65535, 25700) = DIV_ROUND_CLOSEST(25700 * 255, 65535)
+	 * = 100, which falls between the (50, 20) and (128, 50) points:
+	 *   lum = 20 + DIV_ROUND_CLOSEST((50 - 20) * (100 - 50), 128 - 50) = 39
+	 * The curved firmware level is DIV_ROUND_CLOSEST(39 * 100, 101) = 39,
+	 * which is DIV_ROUND_CLOSEST(39 * 65535, 255) = 10023 in the userspace
+	 * domain and 3084 + DIV_ROUND_CLOSEST(62451 * 10023, 65535) = 12635
+	 * once converted to the firmware domain.
+	 */
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 25700), (u32)12635);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_max - The top of the range stays inside the range
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_max(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	/*
+	 * scale_input_to_fw(65535, 65535) = 255 matches the last point exactly,
+	 * so lum = 100 and the curved firmware level is
+	 * DIV_ROUND_CLOSEST(100 * 255, 101) = 252.  That is
+	 * DIV_ROUND_CLOSEST(252 * 65535, 255) = 64764 in the userspace domain
+	 * and 3084 + DIV_ROUND_CLOSEST(62451 * 64764, 65535) = 64800 in the
+	 * firmware domain.
+	 */
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)64800);
+	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_monotonic - A rising curve gives a rising level
+ * @test: The KUnit test context
+ */
+static void dm_test_curve_from_user_monotonic(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max, i;
+	u32 previous = 0;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	for (i = 0; i <= max; i += 1023) {
+		u32 level = convert_brightness_from_user(&caps, i);
+
+		KUNIT_ASSERT_GE(test, level, previous);
+		previous = level;
+	}
+
+	KUNIT_EXPECT_GE(test, convert_brightness_from_user(&caps, max), previous);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_curve_from_user_within_range - Curved levels never leave [min..max]
+ * @test: The KUnit test context
+ *
+ * The firmware level is programmed through a 16-bit path, so a converted value
+ * above max would wrap and darken the panel.
+ */
+static void dm_test_curve_from_user_within_range(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	unsigned int min, max, i;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	dm_test_curve_caps_init(&caps);
+	get_brightness_range(&caps, &min, &max);
+
+	for (i = 0; i <= max; i += 1023) {
+		u32 level = convert_brightness_from_user(&caps, i);
+
+		KUNIT_ASSERT_GE(test, level, (u32)min);
+		KUNIT_ASSERT_LE(test, level, (u32)max);
+	}
+
+	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_from_user_no_curve_unchanged - The linear path is untouched
+ * @test: The KUnit test context
+ *
+ * Without luminance data the conversion is the plain
+ * min + DIV_ROUND_CLOSEST((max - min) * brightness, max) mapping.
+ */
+static void dm_test_from_user_no_curve_unchanged(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.aux_support = false;
+	caps.min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps.data_points = 0;
+
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 0), (u32)3084);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 16383), (u32)18696);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, 32767), (u32)34309);
+	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)65535);
+}
+
 /**
  * dm_test_brightness_range_zero_signals - Test Brightness range with zero min and max signals
  * @test: The KUnit test context
@@ -1955,6 +2210,15 @@ static struct kunit_case dm_backlight_test_cases[] = {
 	KUNIT_CASE(dm_test_brightness_to_user_above_max),
 	KUNIT_CASE(dm_test_brightness_from_user_midrange),
 	KUNIT_CASE(dm_test_brightness_from_user_with_curve),
+	KUNIT_CASE(dm_test_custom_brightness_user_domain),
+	KUNIT_CASE(dm_test_curve_from_user_zero_is_min),
+	KUNIT_CASE(dm_test_curve_from_user_matches_linear_at_zero),
+	KUNIT_CASE(dm_test_curve_from_user_round_trip_zero),
+	KUNIT_CASE(dm_test_curve_from_user_interpolation),
+	KUNIT_CASE(dm_test_curve_from_user_max),
+	KUNIT_CASE(dm_test_curve_from_user_monotonic),
+	KUNIT_CASE(dm_test_curve_from_user_within_range),
+	KUNIT_CASE(dm_test_from_user_no_curve_unchanged),
 	KUNIT_CASE(dm_test_brightness_range_zero_signals),
 	/* amdgpu_dm_backlight_fill_props */
 	KUNIT_CASE(dm_test_backlight_fill_props_ac_linear),
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
  2026-09-02 22:31   ` [PATCH v2 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
@ 2026-09-02 22:31   ` Andrei Rusu de Castro
  2026-09-03  7:50     ` sashiko-bot
  2026-09-02 22:32   ` [PATCH v2 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro
  3 siblings, 1 reply; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 22:31 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu,
	stable

The power module consumes millipercent and builds its own PWM lookup
table from the ATIF brightness transfer characteristics.

The Linux display manager instead converts the userspace request to a
firmware level, then derives a percentage from that hardware-domain
value. For a non-zero PWM minimum this maps the minimum above zero and
the maximum above 100 percent. It also applies the ATIF curve once in
the display manager and again in the power module.

Pass the original userspace percentage to the power module on PWM
panels. Keep the existing nits conversion for AUX panels, but keep the
resulting millinit value unmasked while the power module derives cached
percentages and PWM levels.

The brightness mask is a final DP source-level quirk. Carry it into the
power module and apply it immediately before an AMD AUX write or after
PWM derivation when the effective hardware handoff is PWM. This
preserves ordinary PWM, forced-PWM OLED, AUX fallback-to-PWM, live AMD
AUX, and mode-change replay without perturbing an intermediate unit.

DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE previously bypassed only the display
manager's copy of the curve. Wire it to the power module's existing
linear bypass and use the selected panel's policy rather than panel
zero's. Label the brightness trace value as millipercent or millinits so
the corrected input domain is explicit.

A live DCN 3.5.1 trace exposed the domain mismatch. KUnit coverage is
added separately.

Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/20260902-brightness-cover-abf809f2@empyreal.works?part=3
Fixes: 3c108046e1d6 ("drm/amd/display: Add power module on Linux")
Cc: stable@vger.kernel.org # 7.2.x
Assisted-by: LLM
Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c |  6 +-
 .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h |  2 +-
 .../display/amdgpu_dm/amdgpu_dm_backlight.c   | 44 ++++++++++++---
 .../display/amdgpu_dm/amdgpu_dm_backlight.h   |  2 +
 .../amd/display/amdgpu_dm/amdgpu_dm_trace.h   |  3 +-
 .../drm/amd/display/modules/inc/mod_power.h   |  1 +
 .../gpu/drm/amd/display/modules/power/power.c |  2 +
 .../drm/amd/display/modules/power/power_abm.c | 55 +++++++++++++++----
 .../amd/display/modules/power/power_helpers.h | 14 +++++
 9 files changed, 107 insertions(+), 22 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 ec483276d753..4e730527be4a 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
@@ -425,7 +425,9 @@ static int amdgpu_dm_init_power_module(struct amdgpu_display_manager *dm)
 			!(amdgpu_dc_feature_mask & DC_DISABLE_FRACTIONAL_PWM_MASK);
 		init_data[i].use_custom_backlight_caps = false;
 		init_data[i].custom_backlight_caps_config_no = 0;
-		init_data[i].use_linear_backlight_curve = false;
+		init_data[i].use_linear_backlight_curve =
+			!!(amdgpu_dc_debug_mask &
+			   DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
 		init_data[i].def_varibright_enable = 0;
 		init_data[i].varibright_level = 0;
 		/*
@@ -436,6 +438,8 @@ static int amdgpu_dm_init_power_module(struct amdgpu_display_manager *dm)
 			dm->backlight_caps[i].min_input_signal * 0x101;
 		init_data[i].max_backlight_pwm =
 			dm->backlight_caps[i].max_input_signal * 0x101;
+		init_data[i].brightness_mask =
+			dm->backlight_caps[i].brightness_mask;
 		init_data[i].min_abm_backlight =
 			dm->backlight_caps[i].min_input_signal * 0x101;
 
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
index 3524931451c8..5969c5fbe480 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
@@ -229,7 +229,7 @@ struct amdgpu_dm_backlight_caps {
 	 */
 	bool aux_support;
 	/**
-	 * @brightness_mask: After deriving brightness, OR it with this mask.
+	 * @brightness_mask: OR this with the final source backlight value.
 	 * Workaround for panels with issues with certain brightness values.
 	 */
 	u32 brightness_mask;
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 424b33573a73..adeb4a7ab7c9 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
@@ -218,6 +218,39 @@ u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
 }
 EXPORT_IF_KUNIT(convert_brightness_to_user);
 
+static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_caps *caps,
+					      u32 brightness)
+{
+	unsigned int min, max;
+
+	if (!get_brightness_range(caps, &min, &max) || max <= min)
+		return 0;
+
+	if (brightness >= max)
+		return 100 * 1000;
+
+	return DIV_ROUND_CLOSEST_ULL((u64)brightness * 100 * 1000, max);
+}
+
+STATIC_IFN_KUNIT
+u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
+					u32 user_brightness)
+{
+	u32 brightness;
+
+	if (!caps)
+		return user_brightness;
+
+	if (!caps->aux_support)
+		return convert_brightness_to_millipercent(caps, user_brightness);
+
+	brightness = convert_brightness_from_user(caps, user_brightness);
+
+	return brightness;
+}
+
+EXPORT_IF_KUNIT(convert_brightness_for_power_module);
+
 STATIC_IFN_KUNIT
 struct dc_stream_state *dm_find_stream_with_link(
 	struct amdgpu_display_manager *dm,
@@ -262,7 +295,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 	bool rc = false, reallow_idle = false;
 	struct drm_connector *connector;
 	struct dc_stream_state *stream;
-	unsigned int min, max;
 
 	list_for_each_entry(connector, &dm->ddev->mode_config.connector_list, head) {
 		struct amdgpu_dm_connector *aconnector = to_amdgpu_dm_connector(connector);
@@ -285,12 +317,9 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 	/* update scratch register */
 	if (bl_idx == 0)
 		amdgpu_atombios_scratch_regs_set_backlight_level(dm->adev, dm->brightness[bl_idx]);
-	brightness = convert_brightness_from_user(caps, dm->brightness[bl_idx]);
 	link = (struct dc_link *)dm->backlight_link[bl_idx];
-
-	/* Apply brightness quirk */
-	if (caps->brightness_mask)
-		brightness |= caps->brightness_mask;
+	brightness = convert_brightness_for_power_module(caps,
+							 dm->brightness[bl_idx]);
 
 	if (trace_amdgpu_dm_brightness_enabled()) {
 		trace_amdgpu_dm_brightness(__builtin_return_address(0),
@@ -314,9 +343,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
 		rc = mod_power_set_backlight_nits(dm->power_module, stream, brightness,
 			AUX_BL_DEFAULT_TRANSITION_TIME_MS, false, true);
 	} else {
-		/* power module uses millipercent */
-		get_brightness_range(caps, &min, &max);
-		brightness = DIV_ROUND_CLOSEST(brightness * 100, (max - min)) * 1000;
 		rc = mod_power_set_backlight_percent(dm->power_module, stream,
 						     brightness, 0, false);
 	}
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
index 90bed0ea5d00..396e7654e299 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.h
@@ -72,6 +72,8 @@ u32 convert_brightness_from_user(const struct amdgpu_dm_backlight_caps *caps,
 				 uint32_t brightness);
 u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
 			       uint32_t brightness);
+u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
+					u32 user_brightness);
 int amdgpu_dm_backlight_get_device_index(struct amdgpu_display_manager *dm,
 					 struct backlight_device *bd);
 void amdgpu_dm_backlight_fill_props(const struct amdgpu_dm_backlight_caps *caps,
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
index f33a2c1e0da5..5e7782f9e89f 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_trace.h
@@ -744,10 +744,11 @@ TRACE_EVENT(amdgpu_dm_brightness,
 		__entry->aux = aux;
 		__entry->ac = ac;
 	),
-	TP_printk("%ps: brightness requested=%u converted=%u aux=%s power=%s",
+	TP_printk("%ps: brightness requested=%u converted=%u unit=%s aux=%s power=%s",
 		  (void *)__entry->function,
 		  (u32)__entry->user_brightness,
 		  (u32)__entry->converted_brightness,
+		  (__entry->aux) ? "millinits" : "millipercent",
 		  (__entry->aux) ? "true" : "false",
 		  (__entry->ac) ? "AC" : "DC"
 	)
diff --git a/drivers/gpu/drm/amd/display/modules/inc/mod_power.h b/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
index 02bee3b1956d..56b41861e164 100644
--- a/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
+++ b/drivers/gpu/drm/amd/display/modules/inc/mod_power.h
@@ -19,6 +19,7 @@ struct mod_power_init_params {
 
 	unsigned int min_backlight_pwm;
 	unsigned int max_backlight_pwm;
+	unsigned int brightness_mask;
 
 	unsigned int min_abm_backlight;
 	unsigned int num_backlight_levels;
diff --git a/drivers/gpu/drm/amd/display/modules/power/power.c b/drivers/gpu/drm/amd/display/modules/power/power.c
index ee15c14a899e..1d5e94893e36 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power.c
+++ b/drivers/gpu/drm/amd/display/modules/power/power.c
@@ -157,6 +157,8 @@ struct mod_power *mod_power_create(struct dc *dc,
 				init_params[inst].use_custom_backlight_caps;
 		core_power->bl_prop[inst].custom_backlight_caps_config_no =
 				init_params[inst].custom_backlight_caps_config_no;
+		core_power->bl_prop[inst].brightness_mask =
+				init_params[inst].brightness_mask;
 
 		// Do not allow less than 101 backlight levels
 		if (init_params[inst].num_backlight_levels < 101)
diff --git a/drivers/gpu/drm/amd/display/modules/power/power_abm.c b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
index 5e86889eaa84..0e00042bced4 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power_abm.c
+++ b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
@@ -3,6 +3,7 @@
 // Copyright 2026 Advanced Micro Devices, Inc.
 
 #include "dm_services.h"
+#include "dm_helpers.h"
 #include "dc.h"
 #include "mod_power.h"
 #include "core_types.h"
@@ -266,6 +267,16 @@ void mod_power_set_backlight_control_type(struct core_power *core_power,
 	core_power->bl_prop[inst].backlight_control_type = backlight_control_type;
 }
 
+STATIC_IFN_KUNIT
+unsigned int backlight_apply_source_mask(struct core_power *core_power,
+					 unsigned int backlight,
+					 unsigned int inst)
+{
+	return backlight | core_power->bl_prop[inst].brightness_mask;
+}
+
+EXPORT_IF_KUNIT(backlight_apply_source_mask);
+
 /* Returns true when the panel uses the VESA AUX backlight control path, which
  * requires zero-anchored linear brightness interpolation.
  */
@@ -529,7 +540,7 @@ static unsigned int backlight_millipercent_to_pwm_legacy(
 		return 0;
 
 	// Bypass the brightness mapping LUT
-	if (core_power->bl_prop->use_linear_backlight_curve) {
+	if (core_power->bl_prop[inst].use_linear_backlight_curve) {
 		pwm = core_power->bl_prop[inst].min_backlight_pwm +
 			(unsigned int) div_u64((unsigned long long) millipercent *
 			core_power->bl_prop[inst].backlight_range,
@@ -615,8 +626,10 @@ static unsigned int backlight_millinit_to_pwm_legacy(
  * 0 nits = 0 PWM and max_brightness_millinits = max_backlight_pwm.
  * Otherwise, falls back to the legacy min→max nits range mapping.
  */
-static unsigned int backlight_millinit_to_pwm(
-		struct core_power *core_power, unsigned int millinit, unsigned int inst)
+STATIC_IFN_KUNIT
+unsigned int backlight_millinit_to_pwm(struct core_power *core_power,
+				       unsigned int millinit,
+				       unsigned int inst)
 {
 	if (!is_vesa_abc(core_power, inst))
 		return backlight_millinit_to_pwm_legacy(core_power, millinit, inst);
@@ -639,6 +652,8 @@ static unsigned int backlight_millinit_to_pwm(
 			core_power->bl_prop[inst].max_brightness_millinits);
 }
 
+EXPORT_IF_KUNIT(backlight_millinit_to_pwm);
+
 static bool validate_ext_backlight_caps(
 		struct dm_acpi_atif_backlight_caps *ext_backlight_caps)
 {
@@ -894,6 +909,7 @@ void mod_power_update_backlight_on_mode_change(
     bool is_hdr)
 {
     struct set_backlight_level_params backlight_level_params = { 0 };
+	unsigned int backlight_millinit;
 
 		/* Cache the panel's backlight control type once at mode-change/init
 		 * time. It is a stable per-panel property (decided in the OS shim
@@ -905,9 +921,15 @@ void mod_power_update_backlight_on_mode_change(
 
 		if ((link->dpcd_sink_ext_caps.bits.hdr_aux_backlight_control == 1 ||
 			link->dpcd_sink_ext_caps.bits.sdr_aux_backlight_control == 1) &&
-			link->backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX)
+			link->backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX) {
+			backlight_millinit =
+				core_power->bl_state[panel_inst].backlight_millinit;
+			backlight_millinit =
+				backlight_apply_source_mask(core_power, backlight_millinit,
+							    panel_inst);
 			dc_link_set_backlight_level_nits(link, core_power->bl_state[panel_inst].isHDR,
-				core_power->bl_state[panel_inst].backlight_millinit, 0);
+				backlight_millinit, 0);
+		}
 
 		backlight_level_params.frame_ramp = 0;
 
@@ -918,11 +940,12 @@ void mod_power_update_backlight_on_mode_change(
 		dc_link_set_backlight_level(link, &backlight_level_params);
 }
 
-static bool set_backlight_millinits_aux(struct core_power *core_power,
-		struct dc_stream_state *stream,
-		unsigned int backlight_millinits,
-		unsigned int transition_time_millisec,
-		unsigned int inst)
+STATIC_IFN_KUNIT
+bool set_backlight_millinits_aux(struct core_power *core_power,
+				 struct dc_stream_state *stream,
+				 unsigned int backlight_millinits,
+				 unsigned int transition_time_millisec,
+				 unsigned int inst)
 {
 	struct dc_link *link = NULL;
 
@@ -939,10 +962,17 @@ static bool set_backlight_millinits_aux(struct core_power *core_power,
 		link->dc->caps.dmub_caps.aux_backlight_support)
 		return true;
 
+	if (link->backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX)
+		backlight_millinits =
+			backlight_apply_source_mask(core_power, backlight_millinits,
+						    inst);
+
 	return dc_link_set_backlight_level_nits(link, core_power->bl_state[inst].isHDR,
 			backlight_millinits, transition_time_millisec);
 }
 
+EXPORT_IF_KUNIT(set_backlight_millinits_aux);
+
 static bool set_backlight(struct core_power *core_power,
 		struct dc_stream_state *stream,
 		struct set_backlight_level_params *backlight_level_params,
@@ -1020,6 +1050,11 @@ void fill_backlight_level_params(struct core_power *core_power,
 
 	if (backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX && !is_hdr)
 		backlight_level_params->control_type = BACKLIGHT_CONTROL_PWM;
+
+	if (backlight_level_params->control_type == BACKLIGHT_CONTROL_PWM)
+		backlight_level_params->backlight_pwm_u16_16 =
+			backlight_apply_source_mask(core_power, backlight_pwm,
+						    panel_inst);
 }
 
 bool mod_power_set_backlight_nits(struct mod_power *mod_power,
diff --git a/drivers/gpu/drm/amd/display/modules/power/power_helpers.h b/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
index 68679fa10946..d4d8ebee1cde 100644
--- a/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
+++ b/drivers/gpu/drm/amd/display/modules/power/power_helpers.h
@@ -94,6 +94,7 @@ struct pwr_backlight_properties {
 	unsigned int min_backlight_pwm;
 	unsigned int max_backlight_pwm;
 	unsigned int backlight_range;
+	unsigned int brightness_mask;
 
 	/* Describes the panel's min and max luminance in millinits measured
 	 * on full white screen, in min and max backlight settings.
@@ -203,6 +204,19 @@ unsigned int backlight_millipercent_to_pwm(
 		struct core_power *core_power, unsigned int millipercent, unsigned int inst);
 unsigned int backlight_millipercent_to_millinit(
 		struct core_power *core_power, unsigned int millipercent, unsigned int inst);
+#if IS_ENABLED(CONFIG_DRM_AMD_DC_KUNIT_TEST)
+unsigned int backlight_millinit_to_pwm(struct core_power *core_power,
+				       unsigned int millinit,
+				       unsigned int inst);
+unsigned int backlight_apply_source_mask(struct core_power *core_power,
+					 unsigned int backlight,
+					 unsigned int inst);
+bool set_backlight_millinits_aux(struct core_power *core_power,
+				 struct dc_stream_state *stream,
+				 unsigned int backlight_millinits,
+				 unsigned int transition_time_millisec,
+				 unsigned int inst);
+#endif
 void fill_backlight_level_params(struct core_power *core_power,
 	struct set_backlight_level_params *backlight_level_params,
 	int panel_inst, uint8_t aux_inst, unsigned int backlight_pwm,
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 4/4] drm/amd/display: test power module brightness input domain
  2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
                     ` (2 preceding siblings ...)
  2026-09-02 22:31   ` [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
@ 2026-09-02 22:32   ` Andrei Rusu de Castro
  3 siblings, 0 replies; 12+ messages in thread
From: Andrei Rusu de Castro @ 2026-09-02 22:32 UTC (permalink / raw)
  To: amd-gfx
  Cc: harry.wentland, sunpeng.li, siqueira, alexander.deucher,
	christian.koenig, airlied, simona, alex.hung, roman.li,
	mario.limonciello, dri-devel, linux-kernel, chen-yu.chen, ray.wu

Exercise the exact values selected for the power module and the final
PWM and AMD AUX handoffs. Cover non-zero and zero PWM minimums, both
endpoints, an interior value, clamping, invalid caps, custom-curve
retention, multiple panels, and the source-level brightness mask.

The PWM case carries valid ATIF points to prove that the display manager
passes the userspace percentage without applying that curve before the
power module applies its own lookup table. Additional cases prove that
the brightness mask follows the final hardware domain: the mathematical
AUX millinit remains unmasked, direct AMD AUX receives the mask, and an
AUX fallback derives PWM from the clean millinit before applying the
final PWM mask.

The composition case uses an input for which masking the intermediate
millinit changes the derived PWM value. It fails against the v1
implementation and passes when the mask is applied only at the final
hardware handoffs. Callback captures independently verify the live AMD
AUX value, the unchanged VESA AUX value, and the mode-change replay
value. The selected panel's linear-curve bypass is covered independently.

The cases were verified under UML KUnit.

Assisted-by: LLM
Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 .../tests/amdgpu_dm_backlight_test.c          | 334 +++++++++++++++++-
 1 file changed, 331 insertions(+), 3 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 a6fa052a8272..7ab47a66c915 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
@@ -23,6 +23,7 @@
 #include "amdgpu_dm_kunit_test_helpers.h"
 #include "amd_shared.h"
 #include "link_service.h"
+#include "modules/power/power_helpers.h"
 #include "dc/inc/hw/panel_cntl.h"
 
 struct dm_backlight_connector_fixture {
@@ -31,6 +32,33 @@ struct dm_backlight_connector_fixture {
 	struct dc_link *link;
 };
 
+struct dm_backlight_aux_capture {
+	unsigned int calls;
+	unsigned int backlight_millinits;
+	unsigned int transition_time_millisec;
+	bool is_hdr;
+};
+
+static struct dm_backlight_aux_capture *dm_backlight_aux_capture;
+
+static bool dm_test_capture_backlight_nits(struct dc_link *link, bool is_hdr,
+					   u32 backlight_millinits,
+					   u32 transition_time_in_ms)
+{
+	dm_backlight_aux_capture->calls++;
+	dm_backlight_aux_capture->backlight_millinits = backlight_millinits;
+	dm_backlight_aux_capture->transition_time_millisec = transition_time_in_ms;
+	dm_backlight_aux_capture->is_hdr = is_hdr;
+
+	return true;
+}
+
+static bool dm_test_capture_backlight_level(const struct dc_link *link,
+					    struct set_backlight_level_params *params)
+{
+	return true;
+}
+
 static const struct drm_connector_funcs dm_backlight_test_connector_funcs = {
 	.reset = drm_atomic_helper_connector_reset,
 	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
@@ -1019,6 +1047,296 @@ static void dm_test_brightness_from_user_aux(struct kunit *test)
 	KUNIT_EXPECT_EQ(test, convert_brightness_from_user(&caps, max), (u32)max);
 }
 
+/* Tests for convert_brightness_for_power_module() */
+
+/**
+ * dm_test_power_module_brightness_invalid_caps - Test invalid PWM range
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_brightness_invalid_caps(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(NULL, 100), 100U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 100), 0U);
+}
+
+/**
+ * dm_test_power_module_pwm_uses_user_domain - Test PWM input domain
+ * @test: The KUnit test context
+ *
+ * The power module owns the ATIF luminance-to-PWM curve. The display manager
+ * must therefore pass the original userspace percentage rather than first
+ * converting it to a firmware PWM level.
+ */
+static void dm_test_power_module_pwm_uses_user_domain(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.min_input_signal = AMDGPU_DM_DEFAULT_MIN_BACKLIGHT;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	caps.data_points = 3;
+	caps.luminance_data[0].input_signal = 50;
+	caps.luminance_data[0].luminance = 20;
+	caps.luminance_data[1].input_signal = 128;
+	caps.luminance_data[1].luminance = 50;
+	caps.luminance_data[2].input_signal = 230;
+	caps.luminance_data[2].luminance = 90;
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, min, 3084U);
+	KUNIT_EXPECT_EQ(test, max, 65535U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 0), 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 25700), 39216U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 32768), 50001U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max), 100000U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max + 1), 100000U);
+}
+
+/**
+ * dm_test_power_module_pwm_zero_min - Test zero-minimum range
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_pwm_zero_min(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	unsigned int min, max;
+
+	caps.min_input_signal = 0;
+	caps.max_input_signal = AMDGPU_DM_DEFAULT_MAX_BACKLIGHT;
+	get_brightness_range(&caps, &min, &max);
+
+	KUNIT_EXPECT_EQ(test, min, 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 0), 0U);
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, max), 100000U);
+}
+
+/**
+ * dm_test_power_module_mask_follows_effective_control - Test mask handoff
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_mask_follows_effective_control(struct kunit *test)
+{
+	struct set_backlight_level_params params = {};
+	struct core_power core_power = {};
+
+	core_power.bl_prop[0].brightness_mask = 3;
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3087U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 32896,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 32899U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 65535,
+				    BACKLIGHT_CONTROL_PWM, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 65535U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_AMD_AUX, 0, 0, true);
+	KUNIT_EXPECT_EQ(test, params.control_type, BACKLIGHT_CONTROL_AMD_AUX);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3084U);
+
+	fill_backlight_level_params(&core_power, &params, 0, 0, 3084,
+				    BACKLIGHT_CONTROL_AMD_AUX, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, params.control_type, BACKLIGHT_CONTROL_PWM);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 3087U);
+}
+
+/**
+ * dm_test_power_module_linear_curve_uses_panel_instance - Test linear bypass
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_linear_curve_uses_panel_instance(struct kunit *test)
+{
+	struct core_power core_power = {};
+	unsigned int backlight_lut[101] = {};
+
+	core_power.bl_prop[0].backlight_lut = backlight_lut;
+	core_power.bl_prop[0].num_backlight_levels = ARRAY_SIZE(backlight_lut);
+	core_power.bl_prop[1].min_backlight_pwm = 3084;
+	core_power.bl_prop[1].max_backlight_pwm = 65535;
+	core_power.bl_prop[1].backlight_range = 62451;
+	core_power.bl_prop[1].use_linear_backlight_curve = true;
+	backlight_lut[50] = 12345;
+
+	KUNIT_EXPECT_EQ(test, backlight_millipercent_to_pwm(&core_power, 50000, 1),
+			34309U);
+}
+
+/**
+ * dm_test_power_module_aux_keeps_curve_unmasked - Test AUX conversion path
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_aux_keeps_curve_unmasked(struct kunit *test)
+{
+	struct amdgpu_dm_backlight_caps caps = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	caps.aux_support = true;
+	caps.aux_min_input_signal = 1;
+	caps.aux_max_input_signal = 512;
+	caps.brightness_mask = 3;
+	caps.data_points = 2;
+	caps.luminance_data[0].input_signal = 50;
+	caps.luminance_data[0].luminance = 20;
+	caps.luminance_data[1].input_signal = 200;
+	caps.luminance_data[1].luminance = 80;
+
+	KUNIT_EXPECT_EQ(test, convert_brightness_for_power_module(&caps, 200000),
+			81157U);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_power_module_aux_mask_composition - Compare both final handoffs
+ * @test: The KUnit test context
+ *
+ * AMD AUX writes millinits directly to DP_SOURCE_BACKLIGHT_LEVEL, while its
+ * non-HDR fallback derives a PWM value from the cached millinits. Applying
+ * the source-level mask before that conversion can therefore change the PWM
+ * before the PWM-domain mask is applied.
+ */
+static void dm_test_power_module_aux_mask_composition(struct kunit *test)
+{
+	struct set_backlight_level_params params = {};
+	struct amdgpu_dm_backlight_caps caps = {};
+	struct core_power core_power = {};
+	uint saved_mask = amdgpu_dm_get_dc_debug_mask();
+	u32 aux_level, millinit, pwm;
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask & ~DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE);
+	caps.aux_support = true;
+	caps.aux_min_input_signal = 1;
+	caps.aux_max_input_signal = 512;
+	caps.brightness_mask = 3;
+	caps.data_points = 2;
+	caps.luminance_data[0].input_signal = 50;
+	caps.luminance_data[0].luminance = 20;
+	caps.luminance_data[1].input_signal = 200;
+	caps.luminance_data[1].luminance = 80;
+
+	core_power.bl_prop[0].min_backlight_pwm = 3084;
+	core_power.bl_prop[0].max_backlight_pwm = 65535;
+	core_power.bl_prop[0].backlight_range = 62451;
+	core_power.bl_prop[0].min_brightness_millinits = 1000;
+	core_power.bl_prop[0].max_brightness_millinits = 270000;
+	core_power.bl_prop[0].nits_range = 269000;
+	core_power.bl_prop[0].brightness_mask = 3;
+	core_power.bl_prop[0].backlight_control_type = BACKLIGHT_CONTROL_AMD_AUX;
+
+	millinit = convert_brightness_for_power_module(&caps, 51200);
+	aux_level = backlight_apply_source_mask(&core_power, millinit, 0);
+	pwm = backlight_millinit_to_pwm(&core_power, millinit, 0);
+	fill_backlight_level_params(&core_power, &params, 0, 0, pwm,
+				    BACKLIGHT_CONTROL_AMD_AUX, millinit, 0,
+				    false);
+
+	KUNIT_EXPECT_EQ(test, millinit, 7012U);
+	KUNIT_EXPECT_EQ(test, aux_level, 7015U);
+	KUNIT_EXPECT_EQ(test, pwm, 4479U);
+	KUNIT_EXPECT_EQ(test, params.control_type, BACKLIGHT_CONTROL_PWM);
+	KUNIT_EXPECT_EQ(test, params.backlight_pwm_u16_16, 4479U);
+
+	amdgpu_dm_set_dc_debug_mask(saved_mask);
+}
+
+/**
+ * dm_test_power_module_live_aux_handoff_mask - Test the live AUX callback
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_live_aux_handoff_mask(struct kunit *test)
+{
+	struct dm_backlight_aux_capture *capture;
+	struct link_service *link_srv;
+	struct core_power *core_power;
+	struct dc_stream_state *stream;
+	struct dc_link *link;
+	struct dc *dc;
+
+	capture = kunit_kzalloc(test, sizeof(*capture), GFP_KERNEL);
+	link_srv = kunit_kzalloc(test, sizeof(*link_srv), GFP_KERNEL);
+	core_power = kunit_kzalloc(test, sizeof(*core_power), GFP_KERNEL);
+	stream = kunit_kzalloc(test, sizeof(*stream), GFP_KERNEL);
+	link = kunit_kzalloc(test, sizeof(*link), GFP_KERNEL);
+	dc = kunit_kzalloc(test, sizeof(*dc), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, capture);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, link_srv);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, core_power);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, stream);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, link);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dc);
+
+	dm_backlight_aux_capture = capture;
+	link_srv->edp_set_backlight_level_nits = dm_test_capture_backlight_nits;
+	dc->link_srv = link_srv;
+	link->dc = dc;
+	link->backlight_control_type = BACKLIGHT_CONTROL_AMD_AUX;
+	stream->link = link;
+	core_power->bl_prop[0].brightness_mask = 3;
+
+	KUNIT_ASSERT_TRUE(test, set_backlight_millinits_aux(core_power, stream,
+							    7012, 50, 0));
+	KUNIT_EXPECT_EQ(test, capture->calls, 1U);
+	KUNIT_EXPECT_EQ(test, capture->backlight_millinits, 7015U);
+	KUNIT_EXPECT_EQ(test, capture->transition_time_millisec, 50U);
+	KUNIT_EXPECT_FALSE(test, capture->is_hdr);
+
+	memset(capture, 0, sizeof(*capture));
+	link->backlight_control_type = BACKLIGHT_CONTROL_VESA_AUX;
+	KUNIT_ASSERT_TRUE(test, set_backlight_millinits_aux(core_power, stream,
+							    7012, 50, 0));
+	KUNIT_EXPECT_EQ(test, capture->calls, 1U);
+	KUNIT_EXPECT_EQ(test, capture->backlight_millinits, 7012U);
+
+	dm_backlight_aux_capture = NULL;
+}
+
+/**
+ * dm_test_power_module_mode_change_aux_mask - Test replay AUX callback
+ * @test: The KUnit test context
+ */
+static void dm_test_power_module_mode_change_aux_mask(struct kunit *test)
+{
+	struct dm_backlight_aux_capture *capture;
+	struct link_service *link_srv;
+	struct core_power *core_power;
+	struct dc_link *link;
+	struct dc *dc;
+
+	capture = kunit_kzalloc(test, sizeof(*capture), GFP_KERNEL);
+	link_srv = kunit_kzalloc(test, sizeof(*link_srv), GFP_KERNEL);
+	core_power = kunit_kzalloc(test, sizeof(*core_power), GFP_KERNEL);
+	link = kunit_kzalloc(test, sizeof(*link), GFP_KERNEL);
+	dc = kunit_kzalloc(test, sizeof(*dc), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, capture);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, link_srv);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, core_power);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, link);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dc);
+
+	dm_backlight_aux_capture = capture;
+	link_srv->edp_set_backlight_level_nits = dm_test_capture_backlight_nits;
+	link_srv->edp_set_backlight_level = dm_test_capture_backlight_level;
+	dc->link_srv = link_srv;
+	link->dc = dc;
+	link->backlight_control_type = BACKLIGHT_CONTROL_AMD_AUX;
+	link->dpcd_sink_ext_caps.bits.sdr_aux_backlight_control = 1;
+	core_power->bl_state[0].backlight_millinit = 7012;
+	core_power->bl_prop[0].brightness_mask = 3;
+
+	mod_power_update_backlight_on_mode_change(core_power, link, 0, 0, false);
+	KUNIT_EXPECT_EQ(test, capture->calls, 1U);
+	KUNIT_EXPECT_EQ(test, capture->backlight_millinits, 7015U);
+	KUNIT_EXPECT_EQ(test, capture->transition_time_millisec, 0U);
+
+	dm_backlight_aux_capture = NULL;
+}
+
 /* Tests for convert_custom_brightness() */
 
 /**
@@ -1590,7 +1908,7 @@ static void dm_test_curve_from_user_monotonic(struct kunit *test)
 	for (i = 0; i <= max; i += 1023) {
 		u32 level = convert_brightness_from_user(&caps, i);
 
-		KUNIT_ASSERT_GE(test, level, previous);
+		KUNIT_EXPECT_GE(test, level, previous);
 		previous = level;
 	}
 
@@ -1619,8 +1937,8 @@ static void dm_test_curve_from_user_within_range(struct kunit *test)
 	for (i = 0; i <= max; i += 1023) {
 		u32 level = convert_brightness_from_user(&caps, i);
 
-		KUNIT_ASSERT_GE(test, level, (u32)min);
-		KUNIT_ASSERT_LE(test, level, (u32)max);
+		KUNIT_EXPECT_GE(test, level, (u32)min);
+		KUNIT_EXPECT_LE(test, level, (u32)max);
 	}
 
 	KUNIT_EXPECT_LE(test, convert_brightness_from_user(&caps, max), (u32)max);
@@ -2198,6 +2516,16 @@ static struct kunit_case dm_backlight_test_cases[] = {
 	KUNIT_CASE(dm_test_brightness_from_user_zero),
 	KUNIT_CASE(dm_test_brightness_from_user_max),
 	KUNIT_CASE(dm_test_brightness_from_user_aux),
+	/* convert_brightness_for_power_module */
+	KUNIT_CASE(dm_test_power_module_brightness_invalid_caps),
+	KUNIT_CASE(dm_test_power_module_pwm_uses_user_domain),
+	KUNIT_CASE(dm_test_power_module_pwm_zero_min),
+	KUNIT_CASE(dm_test_power_module_mask_follows_effective_control),
+	KUNIT_CASE(dm_test_power_module_linear_curve_uses_panel_instance),
+	KUNIT_CASE(dm_test_power_module_aux_keeps_curve_unmasked),
+	KUNIT_CASE(dm_test_power_module_aux_mask_composition),
+	KUNIT_CASE(dm_test_power_module_live_aux_handoff_mask),
+	KUNIT_CASE(dm_test_power_module_mode_change_aux_mask),
 	/* convert_custom_brightness */
 	KUNIT_CASE(dm_test_custom_brightness_no_data_points),
 	KUNIT_CASE(dm_test_custom_brightness_debug_mask_disables),
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module
  2026-09-02 22:31   ` [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
@ 2026-09-03  7:50     ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-09-03  7:50 UTC (permalink / raw)
  To: Andrei Rusu de Castro; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

New issues:
- [Medium] The brightness quirk mask is not applied to `backlight_millinits` for AMD AUX panels, causing firmware to write unmasked values during autonomous operations

Pre-existing issues:
- [High] Unprotected traversal of drm_mode_config.connector_list
- [High] Unprotected access to `dm->dc->current_state` leading to potential Use-After-Free
- [High] Potential division by zero in brightness conversion functions when firmware capabilities are anomalous
- [High] Incomplete bounds validation during power module creation allows multi-display setups to trigger a division by zero
--

commit 8608a59a12eb05fd14ffc0e7cb1af582e44dade9
Author: Andrei Rusu de Castro <arc@empyreal.works>
Date:   Wed Sep 2 22:31:59 2026 +0000

drm/amd/display: pass userspace brightness to power module

The patch adjusts how brightness is passed to the power module in amdgpu_dm,
keeping it as userspace percentage (millipercent) instead of converting to
PWM domain first. This fixes domain mismatch issues and handles panel-specific
brightness quirks for AMD AUX writes.

Closes: https://sashiko.dev/#/patchset/20260902-brightness-cover-abf809f2@empyreal.works?part=3

> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_backlight.c
[ ... ]
> @@ -218,6 +218,39 @@ u32 convert_brightness_to_user(const struct amdgpu_dm_backlight_caps *caps,
>  }
>  EXPORT_IF_KUNIT(convert_brightness_to_user);
>  
> +static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_caps *caps,
> +					      u32 brightness)
> +{
> +	unsigned int min, max;
> +
> +	if (!get_brightness_range(caps, &min, &max) || max <= min)
> +		return 0;
> +
> +	if (brightness >= max)
> +		return 100 * 1000;
> +
> +	return DIV_ROUND_CLOSEST_ULL((u64)brightness * 100 * 1000, max);
> +}
> +
> +STATIC_IFN_KUNIT
> +u32 convert_brightness_for_power_module(const struct amdgpu_dm_backlight_caps *caps,
> +					u32 user_brightness)
> +{
> +	u32 brightness;
> +
> +	if (!caps)
> +		return user_brightness;
> +
> +	if (!caps->aux_support)
> +		return convert_brightness_to_millipercent(caps, user_brightness);
> +
> +	brightness = convert_brightness_from_user(caps, user_brightness);

[Severity: High]
This is a pre-existing issue, but while convert_brightness_to_millipercent()
properly guards against max <= min to avoid division by zero, the
convert_brightness_from_user() function called here lacks these same guards.
Does convert_brightness_from_user() unconditionally divide by max, risking a
kernel panic if anomalous firmware sets max to zero? Similarly, does
convert_brightness_to_user() risk dividing by zero when evaluating max - min?

> +
> +	return brightness;
> +}
[ ... ]
> @@ -262,7 +295,6 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
>  	bool rc = false, reallow_idle = false;
>  	struct drm_connector *connector;
>  	struct dc_stream_state *stream;
> -	unsigned int min, max;
>  
>  	list_for_each_entry(connector, &dm->ddev->mode_config.connector_list, head) {

[Severity: High]
This isn't a bug introduced by this patch, but is it safe to traverse the DRM
connector_list using the raw list_for_each_entry macro without holding
mode_config.mutex or using the drm_connector_list_iter API? Can a concurrent
hotplug event removing a connector leave this loop traversing a freed pointer,
resulting in a use-after-free?

[ ... ]
> @@ -285,12 +317,9 @@ void amdgpu_dm_backlight_set_level(struct amdgpu_display_manager *dm,
>  	/* update scratch register */
>  	if (bl_idx == 0)
>  		amdgpu_atombios_scratch_regs_set_backlight_level(dm->adev, dm->brightness[bl_idx]);
> -	brightness = convert_brightness_from_user(caps, dm->brightness[bl_idx]);
>  	link = (struct dc_link *)dm->backlight_link[bl_idx];
> -
> -	/* Apply brightness quirk */
> -	if (caps->brightness_mask)
> -		brightness |= caps->brightness_mask;
> +	brightness = convert_brightness_for_power_module(caps,
> +							 dm->brightness[bl_idx]);

[Severity: High]
This is a pre-existing issue, but further down in this function,
dm_find_stream_with_link() is called before acquiring dc_lock. Inside
dm_find_stream_with_link(), the dm->dc->current_state pointer is dereferenced
and iterated over. Because this sysfs callback runs concurrently with atomic
commits (which free the old state), is there a risk of a use-after-free crash
by reading a stale current_state pointer before the lock is held?

[ ... ]
> --- a/drivers/gpu/drm/amd/display/modules/power/power.c
> +++ b/drivers/gpu/drm/amd/display/modules/power/power.c
> @@ -157,6 +157,8 @@ struct mod_power *mod_power_create(struct dc *dc,
>  				init_params[inst].use_custom_backlight_caps;
>  		core_power->bl_prop[inst].custom_backlight_caps_config_no =
>  				init_params[inst].custom_backlight_caps_config_no;
> +		core_power->bl_prop[inst].brightness_mask =
> +				init_params[inst].brightness_mask;
>  
>  		// Do not allow less than 101 backlight levels
>  		if (init_params[inst].num_backlight_levels < 101)

[Severity: High]
This isn't a bug introduced by this patch, but further down in
mod_power_create(), a loop validates the brightness ranges for eDP instances:

for (inst = 0; inst < edp_num; inst++) {
    if (core_power->bl_prop[inst].nits_range != 0 && ...) {
        is_brightness_range_valid = true;
        break;
    }
}

Does this premature break cause the loop to exit without verifying remaining
instances? If panel 1 has a zero nits_range while panel 0 is valid, could
this lead to a division by zero later in backlight_millinit_to_pwm_legacy()
when it evaluates panel 1?

[ ... ]
> --- a/drivers/gpu/drm/amd/display/modules/power/power_abm.c
> +++ b/drivers/gpu/drm/amd/display/modules/power/power_abm.c
[ ... ]
> @@ -1020,6 +1050,11 @@ void fill_backlight_level_params(struct core_power *core_power,
>  
>  	if (backlight_control_type == BACKLIGHT_CONTROL_AMD_AUX && !is_hdr)
>  		backlight_level_params->control_type = BACKLIGHT_CONTROL_PWM;
> +
> +	if (backlight_level_params->control_type == BACKLIGHT_CONTROL_PWM)
> +		backlight_level_params->backlight_pwm_u16_16 =
> +			backlight_apply_source_mask(core_power, backlight_pwm,
> +						    panel_inst);
>  }

[Severity: Medium]
When backlight_level_params->control_type remains BACKLIGHT_CONTROL_AMD_AUX
(for instance, during HDR playback), the brightness_mask is not applied to
backlight_level_params->backlight_millinits here. Because these parameters
are subsequently passed to the DMCUB firmware for autonomous operations like
PSR/Replay exits, will this allow the firmware to write unmasked brightness
values directly to the panel, bypassing the quirk? Should
backlight_millinits also be masked here when the control type is AMD AUX?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1788388049.git.arc@empyreal.works?part=3

^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-09-03  7:50 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-02 12:31 [PATCH 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
2026-09-02 12:31 ` [PATCH 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
2026-09-02 12:31 ` [PATCH 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
2026-09-02 12:32 ` [PATCH 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
2026-09-02 12:57   ` sashiko-bot
2026-09-02 12:33 ` [PATCH 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro
2026-09-02 22:31 ` [PATCH v2 0/4] drm/amd/display: fix brightness ownership through power module Andrei Rusu de Castro
2026-09-02 22:31   ` [PATCH v2 1/4] drm/amd/display: keep custom brightness curve in userspace domain Andrei Rusu de Castro
2026-09-02 22:31   ` [PATCH v2 2/4] drm/amd/display: test custom brightness with non-zero minimum Andrei Rusu de Castro
2026-09-02 22:31   ` [PATCH v2 3/4] drm/amd/display: pass userspace brightness to power module Andrei Rusu de Castro
2026-09-03  7:50     ` sashiko-bot
2026-09-02 22:32   ` [PATCH v2 4/4] drm/amd/display: test power module brightness input domain Andrei Rusu de Castro

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox