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) <[email protected]> --- 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
