[PATCH 08/11] drm/vkms: add support to non-uniform LUT for internal color curves

Leandro Ribeiro <[email protected]> Tue, 4 Aug 2026 17:33:48 -0300
Newsgroups gmane.linux.kernel,gmane.comp.video.dri.devel
Message-ID <[email protected]>
This extends VKMS internal LUT implementation, allowing it to represent
non-uniform LUTs.

Such LUTs have the X axis. LUTs that come from userspace (gamma LUT) are
always uniform, so they don't have a X axis.

Note: vkms_color_srgb_inv_srgb() error tolerance reduced from 1/255 to
119/65535 ~= 0.46/255. Before this patch, the test compared the results
after quantizing them to 8-bit precision, while the updated test
compares directly in 16-bit. The LUT precision does not change with this
patch.

Signed-off-by: Leandro Ribeiro <[email protected]>
---
 drivers/gpu/drm/vkms/tests/vkms_color_test.c | 20 +----
 drivers/gpu/drm/vkms/vkms_composer.c         | 85 ++++++++++++++++++--
 drivers/gpu/drm/vkms/vkms_drv.h              |  9 +++
 drivers/gpu/drm/vkms/vkms_luts.c             | 57 +++++++++++--
 4 files changed, 139 insertions(+), 32 deletions(-)

diff --git a/drivers/gpu/drm/vkms/tests/vkms_color_test.c b/drivers/gpu/drm/vkms/tests/vkms_color_test.c
index bb9d84377b97..571b1b579310 100644
--- a/drivers/gpu/drm/vkms/tests/vkms_color_test.c
+++ b/drivers/gpu/drm/vkms/tests/vkms_color_test.c
@@ -98,19 +98,6 @@ static void vkms_color_test_get_uniform_lut_index(struct kunit *test)
 		lut_index = get_uniform_lut_index(&test_linear_lut, test_linear_array[i].red);
 		KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(lut_index), i);
 	}
-
-	KUNIT_EXPECT_EQ(test, drm_fixp2int(get_uniform_lut_index(&srgb_eotf, 0x0)), 0x0);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_eotf, 0x0)), 0x0);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_eotf, 0x101)), 0x1);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_eotf, 0x202)), 0x2);
-
-	KUNIT_EXPECT_EQ(test, drm_fixp2int(get_uniform_lut_index(&srgb_inv_eotf, 0x0)), 0x0);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_inv_eotf, 0x0)), 0x0);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_inv_eotf, 0x101)), 0x1);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_inv_eotf, 0x202)), 0x2);
-
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_eotf, 0xfefe)), 0xfe);
-	KUNIT_EXPECT_EQ(test, drm_fixp2int_ceil(get_uniform_lut_index(&srgb_eotf, 0xffff)), 0xff);
 }
 
 static void vkms_color_test_lerp(struct kunit *test)
@@ -136,13 +123,14 @@ static void vkms_color_test_linear(struct kunit *test)
 static void vkms_color_srgb_inv_srgb(struct kunit *test)
 {
 	u16 srgb, final;
+	u16 tolerance = 119;
 
 	for (int i = 0; i < srgb_eotf.lut_length; i++) {
-		srgb = apply_lut_to_channel_value(&srgb_eotf, i * 0x101, LUT_RED);
+		srgb = apply_lut_to_channel_value(&srgb_eotf, srgb_eotf.x[i], LUT_RED);
 		final = apply_lut_to_channel_value(&srgb_inv_eotf, srgb, LUT_RED);
 
-		KUNIT_EXPECT_GE(test, final / 0x101, i - 1);
-		KUNIT_EXPECT_LE(test, final / 0x101, i + 1);
+		KUNIT_EXPECT_GE(test, final, (int)srgb_eotf.x[i] - tolerance);
+		KUNIT_EXPECT_LE(test, final, (int)srgb_eotf.x[i] + tolerance);
 	}
 }
 
diff --git a/drivers/gpu/drm/vkms/vkms_composer.c b/drivers/gpu/drm/vkms/vkms_composer.c
index 1a5e899e5b4a..4ae1ffd31406 100644
--- a/drivers/gpu/drm/vkms/vkms_composer.c
+++ b/drivers/gpu/drm/vkms/vkms_composer.c
@@ -93,27 +93,81 @@ static inline u16 lut_channel_value(const struct drm_color_lut *lut,
 	return 0;
 }
 
+static u16 apply_non_uniform_lut(const struct vkms_color_lut *lut,
+				 u16 channel_value,
+				 enum lut_channel channel)
+{
+	const struct drm_color_lut *lut_y_lo, *lut_y_hi;
+	u16 y_lo, y_hi;
+	u16 x_lo, x_hi;
+	unsigned int lo, hi, mid;
+	s64 t;
+
+	/*
+	 * Handle values out of LUT domain.
+	 */
+	if (channel_value <= lut->x[0])
+		return lut_channel_value(&lut->y[0], channel);
+	if (channel_value >= lut->x[lut->lut_length - 1])
+		return lut_channel_value(&lut->y[lut->lut_length - 1], channel);
+
+	/*
+	 * Binary search to find the largest index lo such that
+	 * x[lo] <= channel_value.
+	 */
+	lo = 0;
+	hi = lut->lut_length - 1;
+	while (lo < hi) {
+		mid = lo + (hi - lo + 1) / 2;
+		if (lut->x[mid] <= channel_value)
+			lo = mid;
+		else
+			hi = mid - 1;
+	}
+	lut_y_lo = &lut->y[lo];
+
+	/*
+	 * As x[0] < channel_value < x[lut_length - 1] and
+	 * x[lo] <= channel_value, lo + 1 is a valid index.
+	 */
+	lut_y_hi = &lut->y[lo + 1];
+
+	x_lo = lut->x[lo];
+	x_hi = lut->x[lo + 1];
+	y_lo = lut_channel_value(lut_y_lo, channel);
+	y_hi = lut_channel_value(lut_y_hi, channel);
+
+	/* Avoid division by zero when two consecutive x values are equal. */
+	if (x_hi == x_lo)
+		return y_lo;
+
+	t = drm_fixp_div(drm_int2fixp(channel_value - x_lo),
+			 drm_int2fixp(x_hi - x_lo));
+
+	return lerp_u16(y_lo, y_hi, t);
+}
+
 VISIBLE_IF_KUNIT s64 get_uniform_lut_index(const struct vkms_color_lut *lut, u16 channel_value)
 {
 	s64 color_channel_fp = drm_int2fixp(channel_value);
 
+	if (lut->x) {
+		DRM_DEBUG_DRIVER("Non-uniform LUT should not use get_uniform_lut_index()");
+		return 0;
+	}
+
 	return drm_fixp_mul(color_channel_fp, lut->channel_value2index_ratio);
 }
 EXPORT_SYMBOL_IF_KUNIT(get_uniform_lut_index);
 
-VISIBLE_IF_KUNIT u16 apply_lut_to_channel_value(const struct vkms_color_lut *lut, u16 channel_value,
-						enum lut_channel channel)
+static u16 apply_uniform_lut(const struct vkms_color_lut *lut,
+			     u16 channel_value,
+			     enum lut_channel channel)
 {
 	const struct drm_color_lut *lut_y_floor, *lut_y_ceil;
 	s64 lut_index = get_uniform_lut_index(lut, channel_value);
 	u16 floor_channel_value, ceil_channel_value;
 
-	/*
-	 * This checks if `struct drm_color_lut` has any gap added by the compiler
-	 * between the struct fields.
-	 */
-	static_assert(sizeof(struct drm_color_lut) == sizeof(__u16) * 4);
-
 	lut_y_floor = &lut->y[drm_fixp2int(lut_index)];
 	if (drm_fixp2int(lut_index) == (lut->lut_length - 1))
 		/* We're at the end of the LUT array, use same value for ceil and floor */
@@ -127,6 +181,21 @@ VISIBLE_IF_KUNIT u16 apply_lut_to_channel_value(const struct vkms_color_lut *lut
 	return lerp_u16(floor_channel_value, ceil_channel_value,
 			lut_index & DRM_FIXED_DECIMAL_MASK);
 }
+
+VISIBLE_IF_KUNIT u16 apply_lut_to_channel_value(const struct vkms_color_lut *lut, u16 channel_value,
+						enum lut_channel channel)
+{
+	/*
+	 * This checks if `struct drm_color_lut` has any gap added by the compiler
+	 * between the struct fields.
+	 */
+	static_assert(sizeof(struct drm_color_lut) == sizeof(__u16) * 4);
+
+	if (lut->x)
+		return apply_non_uniform_lut(lut, channel_value, channel);
+
+	return apply_uniform_lut(lut, channel_value, channel);
+}
 EXPORT_SYMBOL_IF_KUNIT(apply_lut_to_channel_value);
 
 
diff --git a/drivers/gpu/drm/vkms/vkms_drv.h b/drivers/gpu/drm/vkms/vkms_drv.h
index 55a3ea184e44..be81844dfd22 100644
--- a/drivers/gpu/drm/vkms/vkms_drv.h
+++ b/drivers/gpu/drm/vkms/vkms_drv.h
@@ -158,7 +158,16 @@ struct vkms_plane {
 	struct drm_plane base;
 };
 
+/**
+ * struct vkms_color_lut - Driver specific color LUT representation
+ * @x: LUT x-values, must be non-decreasing and may be non-uniformly spaced.
+ *     Only required for non-uniform LUTs.
+ * @y: LUT y-values.
+ * @lut_length: The LUT length.
+ * @channel_value2index_ratio: helper for uniform LUTs (no x-values).
+ */
 struct vkms_color_lut {
+	u16 *x;
 	struct drm_color_lut *y;
 	size_t lut_length;
 	s64 channel_value2index_ratio;
diff --git a/drivers/gpu/drm/vkms/vkms_luts.c b/drivers/gpu/drm/vkms/vkms_luts.c
index 7b0c8eaf83b8..c2d5f9e502ae 100644
--- a/drivers/gpu/drm/vkms/vkms_luts.c
+++ b/drivers/gpu/drm/vkms/vkms_luts.c
@@ -12,7 +12,42 @@
  * https://gitlab.freedesktop.org/hwentland/lutgen
  */
 
-static struct drm_color_lut srgb_array[] = {
+static u16 linear_x[] = {
+	0x0000, 0x0101, 0x0202, 0x0303, 0x0404, 0x0505, 0x0606, 0x0707,
+	0x0808, 0x0909, 0x0a0a, 0x0b0b, 0x0c0c, 0x0d0d, 0x0e0e, 0x0f0f,
+	0x1010, 0x1111, 0x1212, 0x1313, 0x1414, 0x1515, 0x1616, 0x1717,
+	0x1818, 0x1919, 0x1a1a, 0x1b1b, 0x1c1c, 0x1d1d, 0x1e1e, 0x1f1f,
+	0x2020, 0x2121, 0x2222, 0x2323, 0x2424, 0x2525, 0x2626, 0x2727,
+	0x2828, 0x2929, 0x2a2a, 0x2b2b, 0x2c2c, 0x2d2d, 0x2e2e, 0x2f2f,
+	0x3030, 0x3131, 0x3232, 0x3333, 0x3434, 0x3535, 0x3636, 0x3737,
+	0x3838, 0x3939, 0x3a3a, 0x3b3b, 0x3c3c, 0x3d3d, 0x3e3e, 0x3f3f,
+	0x4040, 0x4141, 0x4242, 0x4343, 0x4444, 0x4545, 0x4646, 0x4747,
+	0x4848, 0x4949, 0x4a4a, 0x4b4b, 0x4c4c, 0x4d4d, 0x4e4e, 0x4f4f,
+	0x5050, 0x5151, 0x5252, 0x5353, 0x5454, 0x5555, 0x5656, 0x5757,
+	0x5858, 0x5959, 0x5a5a, 0x5b5b, 0x5c5c, 0x5d5d, 0x5e5e, 0x5f5f,
+	0x6060, 0x6161, 0x6262, 0x6363, 0x6464, 0x6565, 0x6666, 0x6767,
+	0x6868, 0x6969, 0x6a6a, 0x6b6b, 0x6c6c, 0x6d6d, 0x6e6e, 0x6f6f,
+	0x7070, 0x7171, 0x7272, 0x7373, 0x7474, 0x7575, 0x7676, 0x7777,
+	0x7878, 0x7979, 0x7a7a, 0x7b7b, 0x7c7c, 0x7d7d, 0x7e7e, 0x7f7f,
+	0x8080, 0x8181, 0x8282, 0x8383, 0x8484, 0x8585, 0x8686, 0x8787,
+	0x8888, 0x8989, 0x8a8a, 0x8b8b, 0x8c8c, 0x8d8d, 0x8e8e, 0x8f8f,
+	0x9090, 0x9191, 0x9292, 0x9393, 0x9494, 0x9595, 0x9696, 0x9797,
+	0x9898, 0x9999, 0x9a9a, 0x9b9b, 0x9c9c, 0x9d9d, 0x9e9e, 0x9f9f,
+	0xa0a0, 0xa1a1, 0xa2a2, 0xa3a3, 0xa4a4, 0xa5a5, 0xa6a6, 0xa7a7,
+	0xa8a8, 0xa9a9, 0xaaaa, 0xabab, 0xacac, 0xadad, 0xaeae, 0xafaf,
+	0xb0b0, 0xb1b1, 0xb2b2, 0xb3b3, 0xb4b4, 0xb5b5, 0xb6b6, 0xb7b7,
+	0xb8b8, 0xb9b9, 0xbaba, 0xbbbb, 0xbcbc, 0xbdbd, 0xbebe, 0xbfbf,
+	0xc0c0, 0xc1c1, 0xc2c2, 0xc3c3, 0xc4c4, 0xc5c5, 0xc6c6, 0xc7c7,
+	0xc8c8, 0xc9c9, 0xcaca, 0xcbcb, 0xcccc, 0xcdcd, 0xcece, 0xcfcf,
+	0xd0d0, 0xd1d1, 0xd2d2, 0xd3d3, 0xd4d4, 0xd5d5, 0xd6d6, 0xd7d7,
+	0xd8d8, 0xd9d9, 0xdada, 0xdbdb, 0xdcdc, 0xdddd, 0xdede, 0xdfdf,
+	0xe0e0, 0xe1e1, 0xe2e2, 0xe3e3, 0xe4e4, 0xe5e5, 0xe6e6, 0xe7e7,
+	0xe8e8, 0xe9e9, 0xeaea, 0xebeb, 0xecec, 0xeded, 0xeeee, 0xefef,
+	0xf0f0, 0xf1f1, 0xf2f2, 0xf3f3, 0xf4f4, 0xf5f5, 0xf6f6, 0xf7f7,
+	0xf8f8, 0xf9f9, 0xfafa, 0xfbfb, 0xfcfc, 0xfdfd, 0xfefe, 0xffff
+};
+
+static struct drm_color_lut srgb_y[] = {
 	{ 0x0, 0x0, 0x0, 0 },
 	{ 0x13, 0x13, 0x13, 0 },
 	{ 0x27, 0x27, 0x27, 0 },
@@ -271,14 +306,17 @@ static struct drm_color_lut srgb_array[] = {
 	{ 0xffff, 0xffff, 0xffff, 0 },
 };
 
+static_assert(ARRAY_SIZE(linear_x) == ARRAY_SIZE(srgb_y),
+	      "srgb x and y must have the same number of entries");
+
 const struct vkms_color_lut srgb_eotf = {
-	.y = srgb_array,
-	.lut_length = ARRAY_SIZE(srgb_array),
-	.channel_value2index_ratio = 0xff00ffll
+	.x = linear_x,
+	.y = srgb_y,
+	.lut_length = ARRAY_SIZE(srgb_y)
 };
 EXPORT_SYMBOL(srgb_eotf);
 
-static struct drm_color_lut srgb_inv_array[] = {
+static struct drm_color_lut srgb_inv_y[] = {
 	{ 0x0, 0x0, 0x0, 0 },
 	{ 0xcc2, 0xcc2, 0xcc2, 0 },
 	{ 0x15be, 0x15be, 0x15be, 0 },
@@ -537,9 +575,12 @@ static struct drm_color_lut srgb_inv_array[] = {
 	{ 0xffff, 0xffff, 0xffff, 0 },
 };
 
+static_assert(ARRAY_SIZE(linear_x) == ARRAY_SIZE(srgb_inv_y),
+	      "srgb_inv x and y must have the same number of entries");
+
 const struct vkms_color_lut srgb_inv_eotf = {
-	.y = srgb_inv_array,
-	.lut_length = ARRAY_SIZE(srgb_inv_array),
-	.channel_value2index_ratio = 0xff00ffll
+	.x = linear_x,
+	.y = srgb_inv_y,
+	.lut_length = ARRAY_SIZE(srgb_inv_y)
 };
 EXPORT_SYMBOL(srgb_inv_eotf);
-- 
2.55.0