From: Kunal Joshi <kunal1.joshi@intel.com>
To: igt-dev@lists.freedesktop.org
Cc: Kunal Joshi <kunal1.joshi@intel.com>,
Simon Ser <contact@emersion.fr>,
Jani Nikula <jani.nikula@intel.com>,
Karthik B S <karthik.b.s@intel.com>
Subject: [PATCH i-g-t 1/2] lib/igt_connector_helper: Add DRM connector helpers using libdisplay-info
Date: Mon, 11 May 2026 11:13:55 +0530 [thread overview]
Message-ID: <20260511054356.1313884-2-kunal1.joshi@intel.com> (raw)
In-Reply-To: <20260511054356.1313884-1-kunal1.joshi@intel.com>
The upcoming DP tunneling tests need to look up connectors by EDID
serial or MST PATH across re-probes such as suspend/resume, and
select modes by pixel clock rather than resolution. Neither
capability exists in the current IGT helper set.
Add a small connector-enumeration layer backed by libdisplay-info
with look-up-by-name, by-PATH, and by-serial helpers, plus five
mode/output-selection utilities (lowest/highest clock, preferred,
mode-in-list, output-by-id). The dependency is optional; when
missing the helper is simply not built.
Cc: Simon Ser <contact@emersion.fr>
Cc: Jani Nikula <jani.nikula@intel.com>
Cc: Karthik B S <karthik.b.s@intel.com>
Signed-off-by: Kunal Joshi <kunal1.joshi@intel.com>
---
lib/igt_connector_helper.c | 525 +++++++++++++++++++++++++++++++++++++
lib/igt_connector_helper.h | 45 ++++
lib/meson.build | 5 +
meson.build | 3 +
4 files changed, 578 insertions(+)
create mode 100644 lib/igt_connector_helper.c
create mode 100644 lib/igt_connector_helper.h
diff --git a/lib/igt_connector_helper.c b/lib/igt_connector_helper.c
new file mode 100644
index 000000000..cd10b2e91
--- /dev/null
+++ b/lib/igt_connector_helper.c
@@ -0,0 +1,525 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * SECTION:igt_connector_helper
+ * @short_description: Generic DRM connector helpers
+ * @title: Connector Helper
+ * @include: igt_connector_helper.h
+ *
+ * Helpers for DRM connector enumeration, lookup by name / MST PATH /
+ * EDID serial, and post-hotplug reprobing. EDID parsing is delegated to
+ * libdisplay-info (https://gitlab.freedesktop.org/emersion/libdisplay-info).
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <libdisplay-info/info.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+#include "igt_core.h"
+#include "igt_kms.h"
+#include "igt_connector_helper.h"
+
+#define IGT_CONNECTOR_MAX_PROBE 64
+
+/*
+ * Best-effort serial extraction via libdisplay-info.
+ *
+ * di_info_get_serial() returns a malloc'd string built from the EDID
+ * 0xFF descriptor when present, falling back to the 32-bit header
+ * serial number rendered as decimal. NULL means the EDID is unparsable
+ * or contains no serial information at all.
+ */
+static void connector_serial_from_edid(const void *edid_data, size_t edid_size,
+ char *serial, size_t serial_size)
+{
+ struct di_info *info;
+ char *got;
+
+ serial[0] = '\0';
+
+ info = di_info_parse_edid(edid_data, edid_size);
+ if (!info)
+ return;
+
+ got = di_info_get_serial(info);
+ if (got) {
+ snprintf(serial, serial_size, "%s", got);
+ free(got);
+ }
+
+ di_info_destroy(info);
+}
+
+/**
+ * igt_connector_get_connected:
+ * @drm_fd: DRM file descriptor
+ * @connectors: array to store connector IDs; must be valid if @max_connectors > 0
+ * @max_connectors: maximum number of connectors to store
+ *
+ * Walks all DRM connectors and stores the IDs of those currently connected.
+ * Scanning stops once @max_connectors IDs have been collected; later
+ * connectors are not examined.
+ *
+ * Returns: number of connector IDs written to @connectors, capped by
+ * @max_connectors.
+ */
+int igt_connector_get_connected(int drm_fd, uint32_t *connectors,
+ int max_connectors)
+{
+ drmModeRes *res;
+ drmModeConnector *conn;
+ int count = 0;
+ int i;
+
+ igt_assert(max_connectors >= 0);
+ if (max_connectors > 0)
+ igt_assert(connectors);
+
+ res = drmModeGetResources(drm_fd);
+ if (!res)
+ return 0;
+
+ for (i = 0; i < res->count_connectors && count < max_connectors; i++) {
+ conn = drmModeGetConnector(drm_fd, res->connectors[i]);
+ if (!conn)
+ continue;
+
+ if (conn->connection == DRM_MODE_CONNECTED)
+ connectors[count++] = conn->connector_id;
+
+ drmModeFreeConnector(conn);
+ }
+
+ drmModeFreeResources(res);
+ return count;
+}
+
+/**
+ * igt_connector_get_info:
+ * @drm_fd: DRM file descriptor
+ * @connector_id: connector ID
+ * @name: buffer for connector name (mandatory)
+ * @name_size: size of @name; must be > 0
+ * @serial: buffer for EDID-derived serial (mandatory)
+ * @serial_size: size of @serial; must be > 1
+ * @path: buffer for MST PATH property (optional, may be %NULL)
+ * @path_size: size of @path buffer
+ *
+ * Fills connector name, MST PATH and EDID-derived serial for a connector.
+ * @name is always filled when the connector is found and connected.
+ * @serial is filled by libdisplay-info from the EDID blob; left empty if no
+ * EDID is attached or the EDID has no serial information. @path is filled
+ * only if requested and the PATH property exists.
+ *
+ * Success means the connector object exists and is currently connected.
+ * @serial and @path are best-effort and may still be empty on success.
+ *
+ * Returns: %true if the connector was found and is connected, %false
+ * otherwise.
+ */
+bool igt_connector_get_info(int drm_fd, uint32_t connector_id,
+ char *name, size_t name_size,
+ char *serial, size_t serial_size,
+ char *path, size_t path_size)
+{
+ drmModeConnector *conn;
+ drmModePropertyBlobPtr edid_blob = NULL;
+ drmModePropertyPtr prop;
+ uint64_t blob_id = 0;
+ int i;
+
+ igt_assert(name && name_size > 0);
+ igt_assert(serial && serial_size > 1);
+ igt_assert(!path || path_size > 0);
+
+ name[0] = '\0';
+ serial[0] = '\0';
+ if (path)
+ path[0] = '\0';
+
+ conn = drmModeGetConnector(drm_fd, connector_id);
+ if (!conn)
+ return false;
+
+ if (conn->connection != DRM_MODE_CONNECTED) {
+ drmModeFreeConnector(conn);
+ return false;
+ }
+
+ snprintf(name, name_size, "%s-%d",
+ kmstest_connector_type_str(conn->connector_type),
+ conn->connector_type_id);
+
+ for (i = 0; i < conn->count_props; i++) {
+ prop = drmModeGetProperty(drm_fd, conn->props[i]);
+ if (!prop)
+ continue;
+
+ if (strcmp(prop->name, "EDID") == 0)
+ blob_id = conn->prop_values[i];
+
+ drmModeFreeProperty(prop);
+ if (blob_id)
+ break;
+ }
+
+ if (path) {
+ drmModePropertyBlobPtr path_blob;
+
+ path_blob = kmstest_get_path_blob(drm_fd, connector_id);
+ if (path_blob) {
+ snprintf(path, path_size, "%s",
+ (const char *)path_blob->data);
+ drmModeFreePropertyBlob(path_blob);
+ }
+ }
+
+ drmModeFreeConnector(conn);
+
+ if (blob_id) {
+ edid_blob = drmModeGetPropertyBlob(drm_fd, blob_id);
+ if (edid_blob) {
+ connector_serial_from_edid(edid_blob->data,
+ edid_blob->length,
+ serial, serial_size);
+ drmModeFreePropertyBlob(edid_blob);
+ }
+ }
+
+ return true;
+}
+
+/**
+ * igt_connector_find_by_path:
+ * @drm_fd: DRM file descriptor
+ * @path: MST PATH property value to match
+ * @connector_id: output for connector ID (filled on success)
+ *
+ * Finds the currently connected connector whose PATH property matches @path
+ * (exact string comparison). PATH is the preferred stable identity for MST
+ * connectors; connector IDs may change across hotplug events.
+ *
+ * Returns: %true if a matching connected connector was found.
+ */
+bool igt_connector_find_by_path(int drm_fd, const char *path,
+ uint32_t *connector_id)
+{
+ drmModeRes *res;
+ drmModeConnector *conn;
+ drmModePropertyBlobPtr path_blob;
+ bool found = false;
+ int i;
+
+ if (!path || !connector_id)
+ return false;
+
+ res = drmModeGetResources(drm_fd);
+ if (!res)
+ return false;
+
+ for (i = 0; i < res->count_connectors && !found; i++) {
+ conn = drmModeGetConnector(drm_fd, res->connectors[i]);
+ if (!conn)
+ continue;
+
+ if (conn->connection == DRM_MODE_CONNECTED) {
+ path_blob = kmstest_get_path_blob(drm_fd,
+ conn->connector_id);
+ if (path_blob) {
+ const char *blob_str = path_blob->data;
+
+ /*
+ * The kernel NUL-terminates the PATH blob,
+ * but be defensive and bound the compare by
+ * the blob length so a future kernel change
+ * cannot drive us off the end of the buffer.
+ */
+ if (path_blob->length > 0 &&
+ strnlen(blob_str, path_blob->length) <
+ path_blob->length &&
+ strcmp(blob_str, path) == 0) {
+ *connector_id = conn->connector_id;
+ found = true;
+ }
+ drmModeFreePropertyBlob(path_blob);
+ }
+ }
+
+ drmModeFreeConnector(conn);
+ }
+
+ drmModeFreeResources(res);
+
+ if (found)
+ igt_debug("igt_connector: Found connector %u for path '%s'\n",
+ *connector_id, path);
+
+ return found;
+}
+
+/**
+ * igt_connector_find_by_name:
+ * @drm_fd: DRM file descriptor
+ * @name: connector name to match (e.g. "DP-6")
+ * @connector_id: output for connector ID (filled on success)
+ *
+ * Name-based lookup helper. Prefer igt_connector_find_by_path() or
+ * igt_connector_find_by_serial() for MST connectors, since connector
+ * names and IDs may change across hotplug events.
+ *
+ * Returns: %true if a matching connected connector was found.
+ */
+bool igt_connector_find_by_name(int drm_fd, const char *name,
+ uint32_t *connector_id)
+{
+ drmModeRes *res;
+ drmModeConnector *conn;
+ char conn_name[IGT_CONNECTOR_NAME_MAX];
+ bool found = false;
+ int i;
+
+ if (!name || !connector_id)
+ return false;
+
+ res = drmModeGetResources(drm_fd);
+ if (!res)
+ return false;
+
+ for (i = 0; i < res->count_connectors && !found; i++) {
+ conn = drmModeGetConnector(drm_fd, res->connectors[i]);
+ if (!conn)
+ continue;
+
+ snprintf(conn_name, sizeof(conn_name), "%s-%d",
+ kmstest_connector_type_str(conn->connector_type),
+ conn->connector_type_id);
+
+ if (strcmp(conn_name, name) == 0 &&
+ conn->connection == DRM_MODE_CONNECTED) {
+ *connector_id = conn->connector_id;
+ found = true;
+ }
+
+ drmModeFreeConnector(conn);
+ }
+
+ drmModeFreeResources(res);
+
+ if (found)
+ igt_debug("igt_connector: Found connector %u for name '%s'\n",
+ *connector_id, name);
+
+ return found;
+}
+
+/**
+ * igt_connector_find_by_serial:
+ * @drm_fd: DRM file descriptor
+ * @serial: EDID-derived serial string to match
+ * @connector_id: output for connector ID (filled on success)
+ *
+ * Finds a currently connected connector whose EDID serial matches @serial.
+ * EDID serial is the most stable identifier for a physical display and
+ * survives hotplug / suspend-resume connector re-enumeration.
+ *
+ * Returns: %true if a matching connected connector was found.
+ */
+bool igt_connector_find_by_serial(int drm_fd, const char *serial,
+ uint32_t *connector_id)
+{
+ uint32_t connectors[IGT_CONNECTOR_MAX_PROBE];
+ char name[IGT_CONNECTOR_NAME_MAX];
+ char found_serial[IGT_CONNECTOR_SERIAL_MAX];
+ int n, i;
+
+ if (!serial || !serial[0] || !connector_id)
+ return false;
+
+ n = igt_connector_get_connected(drm_fd, connectors,
+ IGT_CONNECTOR_MAX_PROBE);
+ for (i = 0; i < n; i++) {
+ if (!igt_connector_get_info(drm_fd, connectors[i],
+ name, sizeof(name),
+ found_serial, sizeof(found_serial),
+ NULL, 0))
+ continue;
+
+ if (found_serial[0] && strcmp(found_serial, serial) == 0) {
+ *connector_id = connectors[i];
+ igt_debug("igt_connector: Found connector %u (%s) for serial '%s'\n",
+ *connector_id, name, serial);
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * igt_connector_reprobe_all:
+ * @drm_fd: DRM file descriptor
+ *
+ * Issues a GETCONNECTOR ioctl for every connector, asking the kernel for
+ * fresh state. This refreshes the cached connection status and MST topology
+ * visibility from the kernel's perspective, but does not synthesise a
+ * hot-plug event nor guarantee a full hardware reprobe across all driver
+ * paths.
+ */
+void igt_connector_reprobe_all(int drm_fd)
+{
+ drmModeRes *res;
+ drmModeConnector *conn;
+ int i;
+
+ res = drmModeGetResources(drm_fd);
+ if (!res)
+ return;
+
+ for (i = 0; i < res->count_connectors; i++) {
+ conn = drmModeGetConnector(drm_fd, res->connectors[i]);
+ if (conn)
+ drmModeFreeConnector(conn);
+ }
+
+ drmModeFreeResources(res);
+}
+
+/**
+ * igt_connector_find_lowest_clock_mode:
+ * @output: Connected output to inspect.
+ * @out: Filled with the chosen mode on success.
+ *
+ * Picks the mode with the smallest pixel clock from @output's current
+ * connector mode list. Useful for testing fallback paths that need a
+ * mode that consumes minimal bandwidth.
+ *
+ * Returns: true on success, false if the connector has no modes.
+ */
+bool igt_connector_find_lowest_clock_mode(igt_output_t *output,
+ drmModeModeInfo *out)
+{
+ drmModeConnector *c = output->config.connector;
+ int i, best = -1;
+
+ for (i = 0; i < c->count_modes; i++) {
+ if (best < 0 || c->modes[i].clock < c->modes[best].clock)
+ best = i;
+ }
+ if (best < 0)
+ return false;
+
+ *out = c->modes[best];
+ return true;
+}
+
+/**
+ * igt_connector_find_highest_clock_mode_in:
+ * @conn: Freshly probed connector to inspect.
+ * @out: Filled with the chosen mode on success.
+ *
+ * Picks the mode with the largest pixel clock from @conn's mode list.
+ * Operates on a freshly fetched #drmModeConnector rather than an
+ * #igt_output_t so callers can use it after #drmModeGetConnector to
+ * inspect a kernel-filtered list (e.g. after writing a bandwidth cap).
+ *
+ * Returns: true on success, false if @conn has no modes.
+ */
+bool igt_connector_find_highest_clock_mode_in(const drmModeConnector *conn,
+ drmModeModeInfo *out)
+{
+ int i, best = -1;
+
+ for (i = 0; i < conn->count_modes; i++) {
+ if (best < 0 || conn->modes[i].clock > conn->modes[best].clock)
+ best = i;
+ }
+ if (best < 0)
+ return false;
+
+ *out = conn->modes[best];
+ return true;
+}
+
+/**
+ * igt_connector_find_preferred_mode:
+ * @output: Connected output to inspect.
+ * @out: Filled with the chosen mode on success.
+ *
+ * Picks the first mode flagged %DRM_MODE_TYPE_PREFERRED on @output. Falls
+ * back to the first mode in the list when no preferred mode is present.
+ * Unlike #kmstest_get_connector_default_mode this performs no environment
+ * variable overrides, giving deterministic behaviour for tests that need
+ * to reason about which mode they will pick.
+ *
+ * Returns: true on success, false if the connector has no modes.
+ */
+bool igt_connector_find_preferred_mode(igt_output_t *output,
+ drmModeModeInfo *out)
+{
+ drmModeConnector *c = output->config.connector;
+ int i;
+
+ for (i = 0; i < c->count_modes; i++) {
+ if (c->modes[i].type & DRM_MODE_TYPE_PREFERRED) {
+ *out = c->modes[i];
+ return true;
+ }
+ }
+ if (c->count_modes > 0) {
+ *out = c->modes[0];
+ return true;
+ }
+ return false;
+}
+
+/**
+ * igt_connector_mode_in_list:
+ * @conn: Connector whose mode list to search.
+ * @mode: Mode to look up.
+ *
+ * Returns true if a mode matching @mode in pixel clock and active region
+ * (hdisplay, vdisplay) is present in @conn's current mode list. Useful
+ * for verifying whether a previously cached mode survives a kernel-side
+ * mode filter (e.g. after writing a bandwidth cap).
+ */
+bool igt_connector_mode_in_list(const drmModeConnector *conn,
+ const drmModeModeInfo *mode)
+{
+ int i;
+
+ for (i = 0; i < conn->count_modes; i++) {
+ if (conn->modes[i].clock == mode->clock &&
+ conn->modes[i].hdisplay == mode->hdisplay &&
+ conn->modes[i].vdisplay == mode->vdisplay)
+ return true;
+ }
+ return false;
+}
+
+/**
+ * igt_connector_find_output_by_id:
+ * @display: Initialised display containing the outputs to search.
+ * @connector_id: DRM connector ID to look for.
+ *
+ * Walks the connected outputs of @display and returns the one whose DRM
+ * connector ID matches @connector_id, or %NULL if no such output exists.
+ *
+ * Returns: matching #igt_output_t pointer or %NULL.
+ */
+igt_output_t *igt_connector_find_output_by_id(igt_display_t *display,
+ uint32_t connector_id)
+{
+ igt_output_t *output;
+
+ for_each_connected_output(display, output) {
+ if (output->config.connector->connector_id == connector_id)
+ return output;
+ }
+
+ return NULL;
+}
diff --git a/lib/igt_connector_helper.h b/lib/igt_connector_helper.h
new file mode 100644
index 000000000..3e2ef7e35
--- /dev/null
+++ b/lib/igt_connector_helper.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#ifndef IGT_CONNECTOR_HELPER_H
+#define IGT_CONNECTOR_HELPER_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <xf86drmMode.h>
+
+#include "igt_kms.h"
+
+#define IGT_CONNECTOR_NAME_MAX 64
+#define IGT_CONNECTOR_SERIAL_MAX 64
+
+int igt_connector_get_connected(int drm_fd, uint32_t *connectors,
+ int max_connectors);
+bool igt_connector_get_info(int drm_fd, uint32_t connector_id,
+ char *name, size_t name_size,
+ char *serial, size_t serial_size,
+ char *path, size_t path_size);
+bool igt_connector_find_by_path(int drm_fd, const char *path,
+ uint32_t *connector_id);
+bool igt_connector_find_by_name(int drm_fd, const char *name,
+ uint32_t *connector_id);
+bool igt_connector_find_by_serial(int drm_fd, const char *serial,
+ uint32_t *connector_id);
+void igt_connector_reprobe_all(int drm_fd);
+
+bool igt_connector_find_lowest_clock_mode(igt_output_t *output,
+ drmModeModeInfo *out);
+bool igt_connector_find_highest_clock_mode_in(const drmModeConnector *conn,
+ drmModeModeInfo *out);
+bool igt_connector_find_preferred_mode(igt_output_t *output,
+ drmModeModeInfo *out);
+bool igt_connector_mode_in_list(const drmModeConnector *conn,
+ const drmModeModeInfo *mode);
+igt_output_t *igt_connector_find_output_by_id(igt_display_t *display,
+ uint32_t connector_id);
+
+#endif /* IGT_CONNECTOR_HELPER_H */
diff --git a/lib/meson.build b/lib/meson.build
index a82aa27dc..46d007eef 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -156,6 +156,11 @@ lib_deps = [
inc = [ inc, include_directories('vendor') ]
+if libdisplay_info.found()
+ lib_deps += libdisplay_info
+ lib_sources += 'igt_connector_helper.c'
+endif
+
if libdrm_nouveau.found()
lib_deps += libdrm_nouveau
lib_sources += [
diff --git a/meson.build b/meson.build
index 79051be21..0e06139b2 100644
--- a/meson.build
+++ b/meson.build
@@ -166,6 +166,9 @@ libpci = dependency('libpci', required : true)
libudev = dependency('libudev', required : true)
glib = dependency('glib-2.0', required : true)
+libdisplay_info = dependency('libdisplay-info', required : false)
+build_info += 'libdisplay-info: @0@'.format(libdisplay_info.found())
+
xmlrpc = dependency('xmlrpc', required : false)
xmlrpc_util = dependency('xmlrpc_util', required : false)
xmlrpc_client = dependency('xmlrpc_client', required : false)
--
2.25.1
next prev parent reply other threads:[~2026-05-11 5:23 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-11 5:43 [PATCH i-g-t 0/2] tests/intel/kms_tbt: Add DP tunneling validation tests Kunal Joshi
2026-05-11 5:43 ` Kunal Joshi [this message]
2026-05-11 5:43 ` [PATCH i-g-t 2/2] " Kunal Joshi
2026-05-12 0:14 ` ✓ i915.CI.BAT: success for " Patchwork
2026-05-12 0:50 ` ✓ Xe.CI.BAT: " Patchwork
2026-05-12 3:20 ` ✗ Xe.CI.FULL: failure " Patchwork
2026-05-12 10:22 ` ✓ i915.CI.Full: success " Patchwork
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260511054356.1313884-2-kunal1.joshi@intel.com \
--to=kunal1.joshi@intel.com \
--cc=contact@emersion.fr \
--cc=igt-dev@lists.freedesktop.org \
--cc=jani.nikula@intel.com \
--cc=karthik.b.s@intel.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox