[PATCH 2/2] tests/intel/kms_usb4_switch: add test to validate dock/undock and switch

Kunal Joshi kunal1.joshi at intel.com
Wed May 21 05:56:19 UTC 2025


Add test suite that validates docking/undocking
of TBT hubs and switch from DP-ALT to TBT or vice-versa.

tbt-dock-undock : dock/undock each *Thunderbolt* port × 3,
		  checking HPD, EDIDs and a green fullscreen fb
tbt-switch-alt  : start on TBT → switch to DP-ALT → verify HPD/EDIDs/modeset                             |
alt-switch-tbt  : start on DP-ALT → switch to TBT → verify HPD/EDIDs/modeset
Signed-off-by: Kunal Joshi <kunal1.joshi at intel.com>
---
 tests/intel/kms_usb4_switch.c | 299 ++++++++++++++++++++++++++++++++++
 tests/meson.build             |   1 +
 2 files changed, 300 insertions(+)
 create mode 100644 tests/intel/kms_usb4_switch.c

diff --git a/tests/intel/kms_usb4_switch.c b/tests/intel/kms_usb4_switch.c
new file mode 100644
index 000000000..05a6d3651
--- /dev/null
+++ b/tests/intel/kms_usb4_switch.c
@@ -0,0 +11,304 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+/**
+ * TEST: kms usb4 switch
+ * Category: Display
+ * Description: Validate docking/undocking of TBT Hub and switch
+ *		from TBT mode to DP-ALT mode and vice-versa
+ * Driver requirement: i915, xe
+ * Mega feature: General Display Features
+ */
+
+/**
+ * SUBTEST: tbt-dock-undock
+ * Description: Repeatedly dock and undock each Thunderbolt (TBT) port
+ *              `DOCK_UNDOCK_COUNT` times, verifying hot-plug events,
+ *              EDID (re)appearance and successful full modesets.
+
+ * SUBTEST: tbt-switch-alt
+ * Description: Start on a TBT port, switch the USB4 switch to a
+ *              DisplayPort-Alt-Mode (DP-ALT) port, and validate hot-plug,
+ *              EDIDs and modeset on the new path.
+
+ * SUBTEST: alt-switch-tbt
+ * Description: Start on a DP-ALT port, switch the USB4 switch back to a
+ *              TBT port, and validate hot-plug, EDIDs and modeset on the
+ *              restored path.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <libudev.h>
+#include <drm_fourcc.h>
+#include <xf86drmMode.h>
+
+#include "igt.h"
+#include "igt_kms.h"
+#include "igt_fb.h"
+#include "igt_usb4_switch.h"
+
+#define DOCK_UNDOCK_COUNT 3
+#define HOTPLUG_TIMEOUT 20
+
+typedef struct {
+	int drm_fd;
+	igt_display_t display;
+	struct udev_monitor *hotplug_mon;
+	usb4_switch_t sw;
+	struct usb4_switch_port_info *ports;
+	size_t n_ports;
+	int n_pipes;
+	int current_port;
+} data_t;
+
+static bool fit_modes_in_bw(data_t *d)
+{
+	int ret = igt_display_try_commit_atomic(&d->display,
+						DRM_MODE_ATOMIC_TEST_ONLY |
+						DRM_MODE_ATOMIC_ALLOW_MODESET,
+						NULL);
+	if (ret != 0) {
+		bool ok = igt_override_all_active_output_modes_to_fit_bw(&d->display);
+
+		igt_require_f(ok, "Cannot fit modes in available BW\n");
+	}
+	return true;
+}
+
+static void setup_pipe_on_outputs(data_t *d,
+				  igt_output_t *outputs[],
+				  int output_count)
+{
+	int i = 0;
+	enum pipe pipe;
+
+	igt_require_f(d->n_pipes >= output_count,
+		      "Need %d pipes for %d outputs\n",
+		      d->n_pipes, output_count);
+
+	for_each_pipe(&d->display, pipe) {
+		if (i >= output_count)
+			break;
+		igt_info("Assigning pipe %s → %s\n",
+			 kmstest_pipe_name(pipe),
+			 igt_output_name(outputs[i]));
+		igt_output_set_pipe(outputs[i++], pipe);
+	}
+}
+
+static bool modeset_on_edid_connectors(data_t *d,
+				       char **edids,
+				       int n_edids)
+{
+	igt_output_t *output;
+	igt_output_t *outputs[IGT_MAX_PIPES];
+	int out_cnt = 0;
+	drmModeModeInfo * modes[IGT_MAX_PIPES];
+	struct igt_fb fbs[IGT_MAX_PIPES];
+	struct igt_plane *primaries[IGT_MAX_PIPES];
+	int i;
+
+	/* Find all connected outputs whose EDID matches this port */
+	for_each_connected_output(&d->display, output) {
+		for (i = 0; i < n_edids; i++) {
+			if (usb4_switch_port_has_edid(d->drm_fd,
+						      d->current_port,
+						      edids[i])) {
+				outputs[out_cnt++] = output;
+				igt_info("Port %d → connector %s (EDID %s)\n",
+					 d->current_port, igt_output_name(output),
+					 edids[i]);
+				break;
+			}
+		}
+	}
+	igt_require_f(out_cnt > 0, "No connectors found for EDIDs on port %d\n",
+		      d->current_port);
+
+	/* Reset display and assign pipes */
+	igt_display_reset(&d->display);
+	setup_pipe_on_outputs(d, outputs, out_cnt);
+
+	/* Create simple color framebuffers on each plane */
+	for (i = 0; i < out_cnt; i++) {
+		modes[i] = igt_output_get_mode(outputs[i]);
+		primaries[i] = igt_output_get_plane_type(outputs[i],
+							 DRM_PLANE_TYPE_PRIMARY);
+		igt_create_color_fb(d->drm_fd,
+				    modes[i]->hdisplay,
+				    modes[i]->vdisplay,
+				    DRM_FORMAT_XRGB8888,
+				    DRM_FORMAT_MOD_LINEAR,
+				    0.0, 1.0, 0.0,
+				    &fbs[i]);
+		igt_plane_set_fb(primaries[i], &fbs[i]);
+	}
+
+	/* Fit modes in available bandwidth */
+	igt_require_f(fit_modes_in_bw(d),
+		      "Failed BW fit on port %d\n",
+		      d->current_port);
+
+	/* Commit the atomic modeset */
+	igt_display_commit2(&d->display, COMMIT_ATOMIC);
+	return true;
+}
+
+/**
+ * Returns the index of the first port whose .type matches 'type', or -1.
+ */
+static int find_port_by_type(data_t *d, const char *type)
+{
+	for (size_t i = 0; i < d->n_ports; i++)
+		igt_info("Configured port: %s (looking for %s)\n",
+			 d->ports[i].type, type);
+
+	for (size_t i = 0; i < d->n_ports; i++) {
+		if (strcmp(d->ports[i].type, type) == 0)
+			return i;
+	}
+	return -1;
+}
+
+/**
+ * Perform a dock/undock or switch action on @port_id:
+ *  - set the switch
+ *  - wait for hotplug
+ *  - verify EDID presence/absence
+ *  - full modeset on connectors for that port (only on dock)
+ */
+static void port_action(data_t *d, int port_id,
+			char **edids, int n_edids,
+			bool connect, bool reset)
+{
+	const char *verb = connect ? "Docking" : "Undocking";
+
+	d->hotplug_mon = igt_watch_uevents();
+	igt_assert(d->hotplug_mon);
+	igt_info(" → %s (port %d)\n", verb, port_id);
+
+	/* Trigger the switch */
+	igt_assert_eq(usb4_switch_set_port(&d->sw, port_id), 0);
+	if (!reset)
+		igt_assert(igt_hotplug_detected(d->hotplug_mon, HOTPLUG_TIMEOUT));
+	igt_flush_uevents(d->hotplug_mon);
+	for (int e = 0; e < n_edids; e++) {
+		bool present = usb4_switch_port_has_edid(d->drm_fd, port_id, edids[e]);
+
+		if (!reset) {
+			if (connect)
+				igt_assert_f(present, "EDID %s not found after dock on port %d\n",
+					     edids[e], port_id);
+			else
+				igt_assert_f(!present, "EDID %s still present after undock on port %d\n",
+					     edids[e], port_id);
+		}
+	}
+
+	if (connect) {
+		d->current_port = port_id;
+		igt_assert_f(modeset_on_edid_connectors(d, edids, n_edids),
+			     "Modeset failed for port %d after EDID check\n",
+			     port_id);
+	}
+}
+
+/**
+ * For a single port @p, repeat dock/undock DOCK_UNDOCK_COUNT times.
+ */
+static void test_dock_undock(data_t *d,
+			     struct usb4_switch_port_info *p)
+{
+	/* initial undock */
+	port_action(d, 0, p->edids, p->n_edids, false, true);
+
+	for (int rep = 0; rep < DOCK_UNDOCK_COUNT; rep++) {
+		igt_info("Iteration %d/%d on %s\n", rep+1, DOCK_UNDOCK_COUNT, p->name);
+		port_action(d, p->port_id, p->edids, p->n_edids, true, false);
+		port_action(d, 0, p->edids, p->n_edids, false, false);
+	}
+}
+
+/**
+ * Switch from src to dst port, verifying connectors at each step.
+ */
+static void test_switch(data_t *d,
+			struct usb4_switch_port_info *src,
+			struct usb4_switch_port_info *dst)
+{
+	/* dock src, then switch to dst */
+	port_action(d, src->port_id, src->edids, src->n_edids, true, false);
+	port_action(d, dst->port_id, dst->edids, dst->n_edids, true, false);
+}
+
+igt_main
+{
+	data_t data = {};
+
+	igt_fixture {
+		enum pipe p;
+
+		data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
+		igt_assert(data.drm_fd >= 0);
+		kmstest_set_vt_graphics_mode();
+		igt_display_require(&data.display, data.drm_fd);
+		data.n_pipes = 0;
+		for_each_pipe(&data.display, p)
+			data.n_pipes++;
+
+		if (!usb4_switch_load_config())
+			igt_skip("No USB4Switch section in ~/.igtrc\n");
+
+		data.ports = usb4_switch_list_ports(&data.n_ports);
+		igt_require(data.n_ports > 0);
+
+		igt_assert_eq(usb4_switch_open(&data.sw), 0);
+	}
+
+	igt_subtest_with_dynamic("tbt-dock-undock") {
+		bool saw = false;
+
+		for (size_t i = 0; i < data.n_ports; i++) {
+			if (strcmp(data.ports[i].type, "TBT") != 0)
+				continue;
+			saw = true;
+			igt_dynamic_f("%s", data.ports[i].name)
+				test_dock_undock(&data, &data.ports[i]);
+		}
+		if (!saw)
+			igt_skip("No TBT ports configured\n");
+	}
+
+	igt_subtest("tbt-switch-alt") {
+		int tbt = find_port_by_type(&data, "TBT");
+		int alt = find_port_by_type(&data, "DP-ALT");
+
+		igt_require_f(tbt != -1 && alt != -1,
+			      "Need one TBT and one DP-ALT port configured\n");
+		test_switch(&data, &data.ports[tbt], &data.ports[alt]);
+	}
+
+	igt_subtest("alt-switch-tbt") {
+		int tbt = find_port_by_type(&data, "TBT");
+		int alt = find_port_by_type(&data, "DP-ALT");
+
+		igt_require_f(tbt != -1 && alt != -1,
+			      "Need one TBT and one DP-ALT port configured\n");
+		test_switch(&data, &data.ports[alt], &data.ports[tbt]);
+	}
+
+	igt_fixture {
+		usb4_switch_close(&data.sw);
+		usb4_switch_free_port_info(data.ports, data.n_ports);
+		usb4_switch_unload_config();
+
+		igt_display_fini(&data.display);
+		close(data.drm_fd);
+		udev_monitor_unref(data.hotplug_mon);
+	}
+}
diff --git a/tests/meson.build b/tests/meson.build
index 55bcf57ec..19a7a5ad6 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -270,6 +270,7 @@ intel_kms_progs = [
 	'kms_psr2_su',
 	'kms_psr_stress_test',
 	'kms_pwrite_crc',
+        'kms_usb4_switch',
 ]
 
 intel_xe_progs = [
-- 
2.25.1



More information about the igt-dev mailing list