[PATCH RFC RFT 4/8] drm/amd/display: Stop rounding PWM brightness to whole percents

From: Hugo Sales

Date: Wed Nov 04 2026 - 04:56:02 EST


Since commit d422a66847a9 ("drm/amd/display: map PWM brightness through
custom backlight curve"), convert_brightness_to_millipercent() rounds the
16-bit user level to an 8-bit signal and the luminance to a whole
percent, so the power module gets at most 101 levels

Interpolate on the unrounded user level and round once, to millipercent.
The curve and its segments stay the same, and a result over 100% is
still capped

Signed-off-by: Hugo Sales <hugo@xxxxxxx>
---
On the GV301QH curve, minimum 12 and 49 points, the 62452 levels from
user_min to user_max gave 99 different millipercent values and now give
62112

The curve's first point is 2% at signal 14, and below it the curve is a
line from 0, so the minimum, signal 12, should get 2% * 12 / 14, which
is 1714 millipercent. It now gets exactly that instead of 2000, and
every other level also gets the curve's value instead of the nearest
whole percent

Since patches 2 and 3 the power module's LUT keeps those steps. On
the same curve the levels reach the panel as 57821 different PWM values
instead of 99

backlight_custom_curve_scale() maps 0..user_max to 0..output_max, with
the curve reaching 100% at signal_max. The two weights of the
interpolation can't go negative. A luminance is a u8, so before the cap
the result is at most 2.55 times output_max. The divisor can pass 32
bits, hence mul_u64_add_u64_div_u64()

With no data points the mapping stays linear. Above the last point the
curve still runs to 100% at max_input_signal, and points above
max_input_signal are still interpolated as before. Past a last point
above max_input_signal the curve runs on to 100% at signal 255. Before,
the divisor wrapped there and the level stayed near that point's
luminance. The top level still gives 100% when a point at
max_input_signal claims less

KUnit: the PWM tests from patch 5 fail without this patch and pass
with it
---
.../amd/display/amdgpu_dm/amdgpu_dm_backlight.c | 79 ++++++++++++----------
1 file changed, 42 insertions(+), 37 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 3b59699e74287..900c17e0285c9 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
@@ -39,6 +39,7 @@
#include "modules/inc/mod_power.h"

#include <linux/backlight.h>
+#include <linux/math64.h>
#include <linux/minmax.h>
#include <linux/power_supply.h>
#include <drm/drm_edid.h>
@@ -128,53 +129,59 @@ static inline u32 scale_fw_to_input(int user_max, u64 input)
return DIV_ROUND_CLOSEST_ULL(input * user_max, AMDGPU_MAX_BL_LEVEL);
}

-static u32 backlight_signal_to_luminance(const struct amdgpu_dm_backlight_caps *caps,
- u32 signal)
+/*
+ * Map user level 0..user_max to 0..output_max through the custom curve,
+ * which ends at 100% at signal_max, or at AMDGPU_MAX_BL_LEVEL when the
+ * last point lies past signal_max. A point at signal s sits at user
+ * level s * user_max / AMDGPU_MAX_BL_LEVEL, so compare
+ * user * AMDGPU_MAX_BL_LEVEL against s * user_max rather than rounding the
+ * user level to 8 bits
+ */
+static u32 backlight_custom_curve_scale(const struct amdgpu_dm_backlight_caps *caps,
+ u32 user, u32 user_max, u32 signal_max,
+ u32 output_max)
{
- u32 lower_signal, upper_signal;
- u32 lower_lum, upper_lum;
+ u64 x, x0 = 0, x1, num, den;
+ u32 l0 = 0, l1 = 100;
int i;

+ user = min(user, user_max);
if (!caps->data_points)
- return DIV_ROUND_CLOSEST(signal * 100, AMDGPU_MAX_BL_LEVEL);
-
- if (signal < caps->luminance_data[0].input_signal) {
- lower_signal = 0;
- lower_lum = 0;
- upper_signal = caps->luminance_data[0].input_signal;
- upper_lum = caps->luminance_data[0].luminance;
- } else if (signal >=
- caps->luminance_data[caps->data_points - 1].input_signal) {
- lower_signal = caps->luminance_data[caps->data_points - 1].input_signal;
- lower_lum = caps->luminance_data[caps->data_points - 1].luminance;
- upper_signal = caps->max_input_signal;
- upper_lum = 100;
+ return DIV_ROUND_CLOSEST_ULL((u64)user * output_max, user_max);
+
+ x = (u64)user * AMDGPU_MAX_BL_LEVEL;
+ for (i = 0; i < caps->data_points; i++) {
+ u64 xi = (u64)caps->luminance_data[i].input_signal * user_max;
+
+ if (x < xi)
+ break;
+ x0 = xi;
+ l0 = caps->luminance_data[i].luminance;
+ }
+
+ if (i < caps->data_points) {
+ x1 = (u64)caps->luminance_data[i].input_signal * user_max;
+ l1 = caps->luminance_data[i].luminance;
} else {
- for (i = 1;
- i < caps->data_points &&
- signal > caps->luminance_data[i].input_signal;
- i++)
- ;
-
- lower_signal = caps->luminance_data[i - 1].input_signal;
- upper_signal = caps->luminance_data[i].input_signal;
- lower_lum = caps->luminance_data[i - 1].luminance;
- upper_lum = caps->luminance_data[i].luminance;
+ x1 = (u64)signal_max * user_max;
+ if (x0 > x1)
+ x1 = (u64)AMDGPU_MAX_BL_LEVEL * user_max;
+ if (x >= x1)
+ return output_max;
}

- if (upper_signal == lower_signal)
- return upper_lum;
+ /* x0 <= x < x1, so neither weight wraps */
+ num = (u64)l0 * (x1 - x) + (u64)l1 * (x - x0);
+ den = 100 * (x1 - x0);

- return lower_lum + DIV_ROUND_CLOSEST((upper_lum - lower_lum) *
- (signal - lower_signal),
- upper_signal - lower_signal);
+ return min_t(u64, mul_u64_add_u64_div_u64(output_max, num, den / 2, den),
+ output_max);
}

static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_caps *caps,
u32 user_brightness)
{
unsigned int user_min, user_max;
- u32 signal, luminance;

if (!get_brightness_range(caps, &user_min, &user_max))
return DIV_ROUND_CLOSEST_ULL((u64)user_brightness * 100000,
@@ -184,10 +191,8 @@ static u32 convert_brightness_to_millipercent(const struct amdgpu_dm_backlight_c
if (amdgpu_dc_debug_mask & DC_DISABLE_CUSTOM_BRIGHTNESS_CURVE)
return DIV_ROUND_CLOSEST_ULL((u64)user_brightness * 100000, user_max);

- signal = scale_input_to_fw(user_max, user_brightness);
- luminance = backlight_signal_to_luminance(caps, signal);
-
- return min_t(u32, luminance * 1000, 100000);
+ return backlight_custom_curve_scale(caps, user_brightness, user_max,
+ caps->max_input_signal, 100000);
}

STATIC_IFN_KUNIT

--
2.56.0