All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2] IGT: Add a luminance test
@ 2026-05-28  5:50 Mario Limonciello (AMD)
  2026-05-29 13:16 ` Jani Nikula
  0 siblings, 1 reply; 2+ messages in thread
From: Mario Limonciello (AMD) @ 2026-05-28  5:50 UTC (permalink / raw)
  To: dri-devel
  Cc: Mario Limonciello, harry.wentland, Louis Chauvet,
	Dmitry Baryshkov, Mario Limonciello (AMD)

This will make sure that luminance can be changed from DRM master
and that it stays in sync with sysfs.

Assisted-by: Claude Sonnet
Signed-off-by: Mario Limonciello (AMD) <superm1@kernel.org>
---
v2:
 * Fix some warnings
 * Handle -EBUSY for DRM_CLIENT_CAP_LUMINANCE
---
 tests/kms_luminance.c | 423 ++++++++++++++++++++++++++++++++++++++++++
 tests/meson.build     |   1 +
 2 files changed, 424 insertions(+)
 create mode 100644 tests/kms_luminance.c

diff --git a/tests/kms_luminance.c b/tests/kms_luminance.c
new file mode 100644
index 000000000..1baa3d9c9
--- /dev/null
+++ b/tests/kms_luminance.c
@@ -0,0 +1,423 @@
+/*
+ * Copyright © 2026 Advanced Micro Devices, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/**
+ * TEST: kms luminance
+ * Category: Display
+ * Description: Test LUMINANCE connector property and backlight takeover
+ * Driver requirement: any
+ * Mega feature: General Display Features
+ */
+
+#include "igt.h"
+#include <dirent.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#define BACKLIGHT_PATH "/sys/class/backlight"
+
+#ifndef DRM_CLIENT_CAP_LUMINANCE
+#define DRM_CLIENT_CAP_LUMINANCE 8
+#endif
+
+/**
+ * SUBTEST: luminance-basic
+ * Description: Verify LUMINANCE is atomic and uses the expected range
+ */
+
+/**
+ * SUBTEST: luminance-sysfs-to-drm
+ * Description: Verify sysfs brightness writes fail during luminance takeover
+ */
+
+/**
+ * SUBTEST: luminance-drm-to-sysfs
+ * Description: Verify DRM LUMINANCE updates sysfs brightness
+ */
+
+typedef struct {
+	int drm_fd;
+	igt_display_t display;
+	igt_output_t *output;
+	char backlight_path[256];
+	int max_brightness;
+} data_t;
+
+static bool find_backlight_for_connector(igt_output_t *output, char *path, size_t path_len)
+{
+	char link_path[512];
+	char backlight_name[256];
+	ssize_t len;
+	DIR *dir;
+	struct dirent *entry;
+	char *slash;
+	size_t needed;
+
+	/* Try to find backlight linked to this connector via sysfs */
+	snprintf(link_path, sizeof(link_path),
+		 "/sys/class/drm/card%d-%s/backlight",
+		 output->display->drm_fd,
+		 igt_output_name(output));
+
+	len = readlink(link_path, backlight_name, sizeof(backlight_name) - 1);
+	if (len > 0) {
+		backlight_name[len] = '\0';
+		/* Extract just the backlight device name */
+		slash = strrchr(backlight_name, '/');
+		if (slash) {
+			needed = strlen(BACKLIGHT_PATH) + 1 + strlen(slash + 1) + 1;
+			if (needed <= path_len)
+				snprintf(path, path_len, "%s/%s", BACKLIGHT_PATH, slash + 1);
+			else
+				return false;
+		} else {
+			needed = strlen(BACKLIGHT_PATH) + 1 + strlen(backlight_name) + 1;
+			if (needed <= path_len)
+				snprintf(path, path_len, "%s/%s", BACKLIGHT_PATH, backlight_name);
+			else
+				return false;
+		}
+		return true;
+	}
+
+	/* Fallback: look for any backlight device (common case: single panel) */
+	dir = opendir(BACKLIGHT_PATH);
+
+	if (!dir)
+		return false;
+
+	while ((entry = readdir(dir)) != NULL) {
+		if (entry->d_name[0] == '.')
+			continue;
+
+		needed = strlen(BACKLIGHT_PATH) + 1 + strlen(entry->d_name) + 1;
+		if (needed > path_len)
+			continue;
+
+		snprintf(path, path_len, "%s/%s", BACKLIGHT_PATH, entry->d_name);
+		closedir(dir);
+		return true;
+	}
+
+	closedir(dir);
+	return false;
+}
+
+static int read_sysfs_int(const char *path)
+{
+	FILE *f;
+	int value = -1;
+
+	f = fopen(path, "r");
+	if (!f)
+		return -1;
+
+	if (fscanf(f, "%d", &value) != 1)
+		value = -1;
+
+	fclose(f);
+	return value;
+}
+
+static int write_sysfs_int(const char *path, int value)
+{
+	int fd;
+	int ret;
+	int saved_errno;
+
+	fd = open(path, O_WRONLY);
+	if (fd < 0)
+		return -errno;
+
+	ret = dprintf(fd, "%d\n", value);
+	if (ret < 0) {
+		saved_errno = errno;
+		close(fd);
+		return -saved_errno;
+	}
+
+	if (close(fd) < 0)
+		return -errno;
+
+	return 0;
+}
+
+static void require_luminance_support(data_t *data)
+{
+	uint64_t value;
+
+	igt_require(igt_has_drm_cap(data->drm_fd, DRM_CLIENT_CAP_ATOMIC));
+	igt_require(drmSetClientCap(data->drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0);
+
+	/* Enable luminance takeover support. */
+	igt_require(drmSetClientCap(data->drm_fd, DRM_CLIENT_CAP_LUMINANCE, 1) == 0);
+
+	/* Find an eDP output with LUMINANCE. */
+	for_each_connected_output(&data->display, data->output) {
+		if (data->output->config.connector->connector_type == DRM_MODE_CONNECTOR_eDP) {
+			if (kmstest_get_property(data->drm_fd,
+						 data->output->config.connector->connector_id,
+						 DRM_MODE_OBJECT_CONNECTOR,
+						 "LUMINANCE",
+						 NULL, &value, NULL)) {
+				igt_require(find_backlight_for_connector(data->output,
+									  data->backlight_path,
+									  sizeof(data->backlight_path)));
+				return;
+			}
+		}
+	}
+
+	igt_skip("No eDP connector with LUMINANCE property found\n");
+}
+
+static void test_luminance_basic(data_t *data)
+{
+	uint32_t prop_id;
+	uint64_t value, range_min, range_max;
+	bool is_atomic;
+	drmModePropertyPtr prop;
+
+	igt_info("Testing LUMINANCE property on %s\n", igt_output_name(data->output));
+
+	igt_assert(kmstest_get_property(data->drm_fd,
+					data->output->config.connector->connector_id,
+					DRM_MODE_OBJECT_CONNECTOR,
+					"LUMINANCE",
+					&prop_id, &value, NULL));
+
+	/* Verify property is atomic */
+	prop = drmModeGetProperty(data->drm_fd, prop_id);
+	igt_assert(prop);
+	is_atomic = !!(prop->flags & DRM_MODE_PROP_ATOMIC);
+	igt_assert_f(is_atomic, "LUMINANCE property must be atomic\n");
+
+	/* Verify range is [0, 65535] */
+	igt_assert_f(prop->count_values == 2, "LUMINANCE must be a range property\n");
+	range_min = prop->values[0];
+	range_max = prop->values[1];
+	igt_assert_f(range_min == 0, "LUMINANCE min must be 0, got %lu\n", range_min);
+	igt_assert_f(range_max == 65535, "LUMINANCE max must be 65535, got %lu\n", range_max);
+
+	igt_info("LUMINANCE property: id=%u, value=%lu, range=[%lu, %lu]\n",
+		 prop_id, value, range_min, range_max);
+
+	drmModeFreeProperty(prop);
+}
+
+static void test_luminance_sysfs_to_drm(data_t *data)
+{
+	char brightness_path[512];
+	char max_brightness_path[512];
+	int original_brightness, current_brightness, new_brightness, max_brightness;
+	uint64_t original_luminance, new_luminance;
+	int test_values[] = {0, 0, 0, 0, 0}; /* Will be filled after reading max_brightness */
+	int ret;
+
+	igt_assert_eq(drmSetClientCap(data->drm_fd, DRM_CLIENT_CAP_LUMINANCE, 1), 0);
+
+	snprintf(brightness_path, sizeof(brightness_path), "%s/brightness", data->backlight_path);
+	snprintf(max_brightness_path, sizeof(max_brightness_path), "%s/max_brightness", data->backlight_path);
+
+	max_brightness = read_sysfs_int(max_brightness_path);
+	igt_assert_f(max_brightness > 0, "Failed to read max_brightness\n");
+
+	/* Read original values */
+	original_brightness = read_sysfs_int(brightness_path);
+	igt_assert_f(original_brightness >= 0, "Failed to read brightness\n");
+
+	kmstest_get_property(data->drm_fd,
+			     data->output->config.connector->connector_id,
+			     DRM_MODE_OBJECT_CONNECTOR,
+			     "LUMINANCE",
+			     NULL, &original_luminance, NULL);
+
+	igt_info("Initial: sysfs=%d/%d, LUMINANCE=%lu\n",
+		 original_brightness, max_brightness, original_luminance);
+
+	/* Test min, quartiles, and max. */
+	test_values[0] = 0;
+	test_values[1] = max_brightness / 4;
+	test_values[2] = max_brightness / 2;
+	test_values[3] = max_brightness * 3 / 4;
+	test_values[4] = max_brightness;
+
+	for (int i = 0; i < 5; i++) {
+		new_brightness = test_values[i];
+		ret = write_sysfs_int(brightness_path, new_brightness);
+		igt_assert_f(ret == -EBUSY,
+			     "Expected sysfs write to fail with -EBUSY after setting DRM_CLIENT_CAP_LUMINANCE, got %d\n",
+			     ret);
+
+		current_brightness = read_sysfs_int(brightness_path);
+		igt_assert_f(current_brightness >= 0,
+			     "Failed to read brightness after rejected sysfs write\n");
+
+		/* Read back DRM state. */
+		kmstest_get_property(data->drm_fd,
+				     data->output->config.connector->connector_id,
+				     DRM_MODE_OBJECT_CONNECTOR,
+				     "LUMINANCE",
+				     NULL, &new_luminance, NULL);
+
+		igt_info("Test %d: rejected sysfs=%d with -EBUSY, brightness=%d, LUMINANCE=%lu\n",
+			 i, new_brightness, current_brightness, new_luminance);
+
+		igt_assert_f(current_brightness == original_brightness,
+			     "Brightness changed after rejected sysfs write: expected %d, got %d\n",
+			     original_brightness, current_brightness);
+		igt_assert_f(new_luminance == original_luminance,
+			     "LUMINANCE changed after rejected sysfs write: expected %lu, got %lu\n",
+			     original_luminance, new_luminance);
+	}
+}
+
+static void test_luminance_drm_to_sysfs(data_t *data)
+{
+	char brightness_path[512];
+	char max_brightness_path[512];
+	int original_brightness, new_brightness, max_brightness, expected_brightness;
+	uint64_t original_luminance, new_luminance, luminance_max;
+	uint64_t test_luminance[] = {0, 0, 0, 0, 0}; /* Will be filled after reading luminance_max */
+	drmModeAtomicReqPtr req;
+	drmModePropertyPtr prop;
+	uint32_t prop_id;
+	int ret;
+
+	snprintf(brightness_path, sizeof(brightness_path), "%s/brightness", data->backlight_path);
+	snprintf(max_brightness_path, sizeof(max_brightness_path), "%s/max_brightness", data->backlight_path);
+
+	max_brightness = read_sysfs_int(max_brightness_path);
+	igt_assert_f(max_brightness > 0, "Failed to read max_brightness\n");
+
+	/* Read original values */
+	original_brightness = read_sysfs_int(brightness_path);
+	igt_assert_f(original_brightness >= 0, "Failed to read brightness\n");
+
+	kmstest_get_property(data->drm_fd,
+			     data->output->config.connector->connector_id,
+			     DRM_MODE_OBJECT_CONNECTOR,
+			     "LUMINANCE",
+			     &prop_id, &original_luminance, NULL);
+
+	/* Get the LUMINANCE range. */
+	prop = drmModeGetProperty(data->drm_fd, prop_id);
+	igt_assert(prop);
+	igt_assert(prop->count_values == 2);
+	luminance_max = prop->values[1];
+	drmModeFreeProperty(prop);
+
+	igt_info("Initial: sysfs=%d/%d, LUMINANCE=%lu/%lu\n",
+		 original_brightness, max_brightness, original_luminance, luminance_max);
+
+	/* Test min, quartiles, and max. */
+	test_luminance[0] = 0;
+	test_luminance[1] = luminance_max / 4;
+	test_luminance[2] = luminance_max / 2;
+	test_luminance[3] = luminance_max * 3 / 4;
+	test_luminance[4] = luminance_max;
+
+	for (int i = 0; i < 5; i++) {
+		new_luminance = test_luminance[i];
+
+		req = drmModeAtomicAlloc();
+		igt_assert(req);
+
+		ret = drmModeAtomicAddProperty(req, data->output->config.connector->connector_id,
+					       prop_id, new_luminance);
+		igt_assert(ret >= 0);
+
+		ret = drmModeAtomicCommit(data->drm_fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
+		igt_assert_f(ret == 0, "Atomic commit failed: %s\n", strerror(errno));
+
+		drmModeAtomicFree(req);
+
+		/* Give the kernel time to update sysfs. */
+		usleep(300000); /* 300ms */
+
+		/* Read back sysfs brightness. */
+		new_brightness = read_sysfs_int(brightness_path);
+		igt_assert_f(new_brightness >= 0, "Failed to read brightness after DRM change\n");
+
+		/* Kernel formula:
+		 * brightness = (luminance * max_brightness) / luminance_max
+		 */
+		expected_brightness = (new_luminance * max_brightness) / luminance_max;
+
+		igt_info("Test %d: LUMINANCE=%lu, sysfs=%d (expected %d)\n",
+			 i, new_luminance, new_brightness, expected_brightness);
+
+		/* Allow small rounding differences. */
+		igt_assert_f(abs(new_brightness - expected_brightness) <= 2,
+			     "sysfs not synchronized: expected %d, got %d (diff: %d)\n",
+			     expected_brightness, new_brightness, abs(new_brightness - expected_brightness));
+	}
+
+	/* Restore original value */
+	req = drmModeAtomicAlloc();
+	igt_assert(req);
+	drmModeAtomicAddProperty(req, data->output->config.connector->connector_id,
+				 prop_id, original_luminance);
+	drmModeAtomicCommit(data->drm_fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
+	drmModeAtomicFree(req);
+	usleep(100000);
+}
+
+int igt_main()
+{
+	data_t data = {};
+
+	igt_fixture() {
+		data.drm_fd = drm_open_driver_master(DRIVER_ANY);
+		igt_require(data.drm_fd >= 0);
+
+		kmstest_set_vt_graphics_mode();
+
+		igt_display_require(&data.display, data.drm_fd);
+		require_luminance_support(&data);
+
+		igt_info("Using backlight: %s\n", data.backlight_path);
+	}
+
+	igt_describe("Verify LUMINANCE is atomic and uses the expected range");
+	igt_subtest("luminance-basic")
+		test_luminance_basic(&data);
+
+	igt_describe("Verify sysfs brightness writes fail during luminance takeover");
+	igt_subtest("luminance-sysfs-to-drm")
+		test_luminance_sysfs_to_drm(&data);
+
+	igt_describe("Verify DRM LUMINANCE updates sysfs brightness");
+	igt_subtest("luminance-drm-to-sysfs")
+		test_luminance_drm_to_sysfs(&data);
+
+	igt_fixture() {
+		igt_display_fini(&data.display);
+		drm_close_driver(data.drm_fd);
+	}
+}
diff --git a/tests/meson.build b/tests/meson.build
index fe0818118..525dcc6d8 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -46,6 +46,7 @@ test_progs = [
 	'kms_hdr',
 	'kms_invalid_mode',
 	'kms_lease',
+	'kms_luminance',
 	'kms_multipipe_modeset',
 	'kms_panel_fitting',
 	'kms_pipe_crc_basic',
-- 
2.54.0


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

* Re: [PATCH v2] IGT: Add a luminance test
  2026-05-28  5:50 [PATCH v2] IGT: Add a luminance test Mario Limonciello (AMD)
@ 2026-05-29 13:16 ` Jani Nikula
  0 siblings, 0 replies; 2+ messages in thread
From: Jani Nikula @ 2026-05-29 13:16 UTC (permalink / raw)
  To: Mario Limonciello (AMD), dri-devel
  Cc: Mario Limonciello, harry.wentland, Louis Chauvet,
	Dmitry Baryshkov, Mario Limonciello (AMD)

On Thu, 28 May 2026, "Mario Limonciello (AMD)" <superm1@kernel.org> wrote:
> This will make sure that luminance can be changed from DRM master
> and that it stays in sync with sysfs.

Please send to igt-dev.

BR,
Jani.

-- 
Jani Nikula, Intel

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

end of thread, other threads:[~2026-05-29 13:16 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-28  5:50 [PATCH v2] IGT: Add a luminance test Mario Limonciello (AMD)
2026-05-29 13:16 ` Jani Nikula

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.