[PATCH i-g-t v3 3/3] tests/intel/kms_dp_linktrain_fallback: Add UHBR-to-HBR fallback subtest

Sowmiya S <[email protected]> Wed, 5 Aug 2026 14:14:27 +0530
Newsgroups org.freedesktop.lists.igt-dev
Message-ID <[email protected]>
Add 'uhbr-to-hbr-fallback' to verify the 128b/132b->8b/10b encoding
transition on UHBR-capable DP connectors by pinning the link at the
highest sustainable UHBR rate, forcing LT_FAILURE_REDUCED_CAPS failures
and asserting the rate drops below UHBR10. Supports SST and MST.

v2: resets MST traversal state per test run
v3: - use i915_dp_is_uhbr_rate()/i915_dp_get_next_lower_rate()
      from lib
    - single UHBR capability gate using max_link_rate after reset
    - add igt_reset_connectors() on pin-loop early-return
    - hard-assert for 'fallback not reached'
    - remove dead run_dsc reset; merge run_* funcs
    - fix link rate log units (10 kbit/s).
    - Extend run_lt_fallback_test() with a force_uhbr parameter
    - and remove the duplicate run_uhbr_to_hbr_fallback_test()

Signed-off-by: Sowmiya S <[email protected]>
---
 tests/intel/kms_dp_linktrain_fallback.c | 198 +++++++++++++++++++++---
 1 file changed, 174 insertions(+), 24 deletions(-)

diff --git a/tests/intel/kms_dp_linktrain_fallback.c b/tests/intel/kms_dp_linktrain_fallback.c
index 44b875ab9..2e72957e1 100644
--- a/tests/intel/kms_dp_linktrain_fallback.c
+++ b/tests/intel/kms_dp_linktrain_fallback.c
@@ -24,6 +24,15 @@
  *
  * SUBTEST: dsc-fallback
  * Description: Test fallback to DSC when BW isn't sufficient
+ *
+ * SUBTEST: uhbr-to-hbr-fallback
+ * Description: Verify fallback from UHBR (128b/132b, link rate >= 10 Gbps) to
+ *              HBR3 or lower (8b/10b) on a UHBR-capable DP connector by
+ *              pinning the link at the highest sustainable UHBR rate, forcing
+ *              repeated link training failures and checking that the rate
+ *              drops below UHBR10. Supports both SST and MST (topology-wide)
+ *              outputs; MST siblings share the physical link so fallback
+ *              applies to the whole topology.
  */
 
 #define RETRAIN_COUNT 1
@@ -347,7 +356,20 @@ static bool fix_link_status_and_recommit(data_t *data,
 	return true;
 }
 
-static void test_fallback(data_t *data, bool is_mst)
+/*
+ * test_fallback:
+ * @force_uhbr: when false (dp-fallback), the output is always counted as
+ *		exercised. When true (uhbr-to-hbr-fallback), the link is first
+ *		pinned at the highest sustainable UHBR rate; the return value
+ *		then reports whether the run was skippable (false, e.g. no
+ *		sustainable UHBR rate) so the caller can turn it into a SKIP.
+ *		A genuine failure (UHBR reached but never dropped to HBR) is
+ *		asserted here rather than returned.
+ *
+ * Returns: true if the output was exercised/verified, false for the
+ *	    skippable cases.
+ */
+static bool test_fallback(data_t *data, bool is_mst, bool force_uhbr)
 {
 	int output_count, retries;
 	int max_link_rate, curr_link_rate, prev_link_rate;
@@ -361,14 +383,84 @@ static void test_fallback(data_t *data, bool is_mst)
 	retries = SPURIOUS_HPD_RETRY;
 
 	igt_display_reset(&data->display);
-	i915_dp_reset_link_params(data->drm_fd, data->output);
-	if (!setup_outputs(data, is_mst, outputs,
-			   &output_count, modes, fbs,
-			   primaries))
-		return;
 
-	igt_info("Testing link training fallback on %s\n",
-		 igt_output_name(data->output));
+	/*
+	 * For the UHBR-to-HBR fallback case, pin the link at the highest
+	 * sustainable UHBR rate before forcing failures. The caller
+	 * (run_lt_fallback_test()) has already reset the link params for the
+	 * UHBR gate, so i915_dp_get_max_link_rate() here returns the full
+	 * sink-negotiated max and not a value already reduced by an earlier
+	 * fallback. If the physical link can't sustain that rate (cable
+	 * limit), step down to the next-lower supported UHBR rate and retry.
+	 * Skip the test if no UHBR rate can be sustained.
+	 */
+	if (force_uhbr) {
+		int try_rate = i915_dp_get_max_link_rate(data->drm_fd,
+							 data->output);
+		char rate_str[16];
+		/* Preserve MST state for fallback retries. */
+		int saved_mst_count = traversed_mst_output_count;
+
+		curr_link_rate = 0;
+		while (i915_dp_is_uhbr_rate(try_rate)) {
+			/* Allow MST re-discovery on every UHBR-rate attempt. */
+			traversed_mst_output_count = saved_mst_count;
+
+			snprintf(rate_str, sizeof(rate_str), "%d", try_rate);
+			i915_dp_set_link_params(data->drm_fd, data->output,
+						rate_str, "auto");
+
+			if (!setup_outputs(data, is_mst, outputs,
+					   &output_count, modes, fbs, primaries)) {
+				i915_dp_reset_link_params(data->drm_fd,
+							  data->output);
+				igt_reset_connectors();
+				return false;
+			}
+
+			curr_link_rate = i915_dp_get_current_link_rate(data->drm_fd,
+								       data->output);
+			if (i915_dp_is_uhbr_rate(curr_link_rate)) {
+				igt_info("Link trained at UHBR (link rate %d) on %s\n",
+					 curr_link_rate,
+					 igt_output_name(data->output));
+				break;
+			}
+
+			igt_info("Link rate %d not sustained (got %d) on %s, trying next-lower UHBR\n",
+				 try_rate, curr_link_rate,
+				 igt_output_name(data->output));
+			igt_display_reset(&data->display);
+
+			try_rate = i915_dp_get_next_lower_rate(data->drm_fd,
+							       data->output,
+							       try_rate);
+		}
+
+		if (!i915_dp_is_uhbr_rate(curr_link_rate)) {
+			igt_info("Output %s cannot sustain any UHBR rate, skipping\n",
+				 igt_output_name(data->output));
+			i915_dp_reset_link_params(data->drm_fd, data->output);
+			return false;
+		}
+
+		/*
+		 * Clear the pin so the forced-failure loop below can reduce the
+		 * rate. This re-enables all link configs, so max_link_rate
+		 * (read next) is the full negotiated max, not the pinned rate.
+		 * The "curr == max" spurious-HPD escape below therefore won't
+		 * trigger on this path, which is fine - we only care that the
+		 * rate drops out of UHBR.
+		 */
+		i915_dp_reset_link_params(data->drm_fd, data->output);
+	} else {
+		i915_dp_reset_link_params(data->drm_fd, data->output);
+
+		if (!setup_outputs(data, is_mst, outputs,
+				   &output_count, modes, fbs,
+				   primaries))
+			return false;
+	}
 	max_link_rate = i915_dp_get_max_link_rate(data->drm_fd, data->output);
 	max_lane_count = i915_dp_get_max_lane_count(data->drm_fd, data->output);
 	prev_link_rate = i915_dp_get_current_link_rate(data->drm_fd, data->output);
@@ -389,8 +481,11 @@ static void test_fallback(data_t *data, bool is_mst)
 
 		if (i915_dp_get_link_retrain_disabled(data->drm_fd,
 						      data->output)) {
+			igt_assert_f(!force_uhbr,
+				     "Link retrain disabled before UHBR to HBR fallback on %s\n",
+				     igt_output_name(data->output));
 			igt_reset_connectors();
-			return;
+			return false;
 		}
 
 		igt_assert_f(wait_for_hotplug_and_check_bad(data->drm_fd,
@@ -421,24 +516,57 @@ static void test_fallback(data_t *data, bool is_mst)
 			     ((curr_link_rate == max_link_rate && curr_lane_count == max_lane_count) && --retries),
 			     "Fallback unsuccessful\n");
 
+		/*
+		 * This subtest only cares about the single UHBR -> HBR
+		 * transition (128b/132b to 8b/10b encoding). Stop as soon as
+		 * we've dropped below UHBR10, instead of cascading all the
+		 * way down through the legacy HBR/HBR2/HBR3 rates, which is
+		 * already covered by dp-fallback.
+		 */
+		if (force_uhbr && !i915_dp_is_uhbr_rate(curr_link_rate)) {
+			igt_info("UHBR to HBR fallback confirmed on %s: link rate %d -> %d\n",
+				 igt_output_name(data->output),
+				 prev_link_rate, curr_link_rate);
+			/*
+			 * Link params are already 'auto' (the pin was cleared
+			 * before this loop) and the per-output UHBR gate in
+			 * run_lt_fallback_test() resets again for the next
+			 * output, so this run is already self-contained.
+			 */
+			return true;
+		}
+
 		prev_link_rate = curr_link_rate;
 		prev_lane_count = curr_lane_count;
 	}
+
+	/*
+	 * force_uhbr: the loop only exits here if link retrain got disabled
+	 * before the rate ever dropped out of UHBR, i.e. we trained at UHBR and
+	 * forced failures but never saw the UHBR -> HBR transition. That is a
+	 * real failure, not a skip.
+	 */
+	igt_assert_f(!force_uhbr,
+		     "UHBR to HBR fallback not reached on %s (last link rate %d)\n",
+		     igt_output_name(data->output), prev_link_rate);
+
+	return true;
 }
 
-static bool run_lt_fallback_test(data_t *data)
+static bool run_lt_fallback_test(data_t *data, bool force_uhbr)
 {
 	bool ran = false;
 	igt_output_t *output;
 
 	/*
 	 * Reset per invocation so MST traversal state from a previous subtest
-	 * (e.g. dp-fallback followed by dsc-fallback) doesn't leak and cause
-	 * MST siblings to be silently skipped as "already visited".
+	 * doesn't leak into this run.
 	 */
 	traversed_mst_output_count = 0;
 
 	for_each_connected_output(&data->display, output) {
+		bool is_mst, tested;
+
 		data->output = output;
 
 		if (!i915_dp_has_force_link_training_failure_debugfs(data->drm_fd,
@@ -450,23 +578,40 @@ static bool run_lt_fallback_test(data_t *data)
 
 		if (output->config.connector->connector_type != DRM_MODE_CONNECTOR_DisplayPort) {
 			igt_info("Skipping output %s as it's not DP\n", output->name);
+			continue;
+		}
+
+		if (force_uhbr) {
+			/*
+			 * Single UHBR-capability gate: reset first so the max
+			 * link rate reflects the full sink-negotiated caps and
+			 * not a value already reduced by an earlier fallback.
+			 */
+			i915_dp_reset_link_params(data->drm_fd, data->output);
+			if (!i915_dp_is_uhbr_rate(i915_dp_get_max_link_rate(data->drm_fd,
+									    data->output))) {
+				igt_info("Skipping output %s: does not support UHBR\n",
+					 igt_output_name(data->output));
 				continue;
+			}
 		}
 
-		ran = true;
+		is_mst = igt_check_output_is_dp_mst(data->output);
+		igt_info("Testing %s%s output %s\n",
+			 force_uhbr ? "UHBR-to-HBR fallback on " : "",
+			 is_mst ? "MST" : "DP",
+			 igt_output_name(data->output));
+
+		tested = test_fallback(data, is_mst, force_uhbr);
 
 		/*
-		 * Check output is MST
+		 * dp-fallback counts every capable output as exercised; the
+		 * UHBR subtest only counts outputs where fallback was actually
+		 * driven (test_fallback() returns false for the skippable
+		 * cases, e.g. no sustainable UHBR rate).
 		 */
-		if (igt_check_output_is_dp_mst(data->output)) {
-			igt_info("Testing MST output %s\n",
-				 igt_output_name(data->output));
-			test_fallback(data, true);
-		} else {
-			igt_info("Testing DP output %s\n",
-				 igt_output_name(data->output));
-			test_fallback(data, false);
-		}
+		if (!force_uhbr || tested)
+			ran = true;
 	}
 	return ran;
 }
@@ -644,7 +789,7 @@ int igt_main()
 	}
 
 	igt_subtest("dp-fallback") {
-		igt_require_f(run_lt_fallback_test(&data),
+		igt_require_f(run_lt_fallback_test(&data, false),
 			      "Skipping test as no output found or none supports fallback\n");
 	}
 
@@ -653,6 +798,11 @@ int igt_main()
 			      "Skipping test as DSC fallback conditions not met.\n");
 	}
 
+	igt_subtest("uhbr-to-hbr-fallback") {
+		igt_require_f(run_lt_fallback_test(&data, true),
+			      "Skipping test: no UHBR-capable DP output found\n");
+	}
+
 	igt_fixture() {
 		igt_remove_fb(data.drm_fd, &data.fb);
 		igt_display_fini(&data.display);
-- 
2.51.0