[PATCH 2/4] drm/amd/display: test custom brightness with non-zero minimum
From: Andrei Rusu de Castro
Date: Wed Sep 02 2026 - 09:27:13 EST
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),