[2/2] tests/intel/kms_usb4_switch: add test to validate dock/undock and switch
Murthy, Arun R
arun.r.murthy at intel.com
Fri Jul 18 08:16:32 UTC 2025
On 21-05-2025 11:26, Kunal Joshi wrote:
> 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);
> + }
> +}
> +
This function is already present in the IGT code, can that be reused
instead of duplicating?
Thanks and Regards,
Arun R Murthy
--------------------
> +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 = [
More information about the igt-dev
mailing list