* [PATCH i-g-t 1/2] tests/intel: Add kms_hdmi_audio_bw test
@ 2026-08-14 12:16 Swati Sharma
0 siblings, 0 replies; 4+ messages in thread
From: Swati Sharma @ 2026-08-14 12:16 UTC (permalink / raw)
To: igt-dev; +Cc: Swati Sharma
Add a new IGT test to validate HDMI TMDS audio bandwidth constraints
under constrained horizontal blanking intervals.
The test injects EDIDs declaring all 7 CEA sample rates (32kHz-192kHz)
and observes which rates the driver exposes via ELD (EDID-Like Data)
under varying BPC and channel configurations.
Subtests:
- audio-bw-supported: Baseline with hblank=160 where audio bandwidth
is sufficient for all configurations.
- audio-bw-pruned: Constrained hblank=80 (CVT RB2) where the driver
must prune unsustainable sample rates or disable audio entirely.
- suspend-s3/s4-audio-recovery: Verify audio state and sample rates
are preserved across system suspend/resume.
- runtime-suspend-audio-recovery: Verify audio state is preserved
across DPMS off/on cycles.
The test uses the HDMI TMDS bandwidth formula from the spec:
pkts_avail = FLOOR((CEIL(hblank * bpc/8) - overhead) / 32)
pkts_reqd = CEIL(R_AP * T_line)
where overhead=74 (HDCP 1.4 rekey always reserved by the driver).
Assertions verify:
- Audio is active when bandwidth is available (pkts_avail > 0)
- Audio is inactive when bandwidth is exhausted (pkts_avail == 0)
- No sample rate requiring more packets than available appears in ELD
Signed-off-by: Swati Sharma <swati2.sharma@intel.com>
Assisted-by: GitHub Copilot:Claude Opus 4.6
---
tests/intel/kms_hdmi_audio_bw.c | 626 ++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 627 insertions(+)
create mode 100644 tests/intel/kms_hdmi_audio_bw.c
diff --git a/tests/intel/kms_hdmi_audio_bw.c b/tests/intel/kms_hdmi_audio_bw.c
new file mode 100644
index 000000000..4b6f0d001
--- /dev/null
+++ b/tests/intel/kms_hdmi_audio_bw.c
@@ -0,0 +1,626 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * TEST: kms hdmi audio bw
+ * Category: Display
+ * Description: Validate HDMI TMDS audio bandwidth constraints by injecting
+ * EDIDs with all sample rates declared and observing which rates
+ * the driver exposes (via ELD) under varying BPC / channel /
+ * hblank configurations.
+ * Driver requirement: i915, xe
+ * Mega feature: Display Audio
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <string.h>
+
+#include "igt.h"
+#include "igt_edid.h"
+#include "igt_eld.h"
+#include "igt_aux.h"
+#include "xe/xe_query.h"
+
+/**
+ * SUBTEST: audio-bw-supported
+ * Description: Baseline test with hblank=160 where audio bandwidth is
+ * sufficient for all BPC and channel combinations. Verifies
+ * that no sample rates are pruned.
+ *
+ * SUBTEST: audio-bw-pruned
+ * Description: Constrained test with hblank=80 (CVT RB2) where audio
+ * bandwidth is limited. Logs which sample rates are pruned
+ * per BPC and channel combination.
+ *
+ * SUBTEST: suspend-%s-audio-recovery
+ * Description: Validate audio state restoration after %arg[1] with
+ * constrained hblank=80 and 12bpc.
+ *
+ * arg[1]:
+ *
+ * @s3: S3 (suspend to RAM)
+ * @s4: S4 (hibernate)
+ *
+ * SUBTEST: runtime-suspend-audio-recovery
+ * Description: Validate audio state restoration after runtime suspend/resume
+ * with constrained hblank=80 and 12bpc.
+ */
+
+IGT_TEST_DESCRIPTION("Validate HDMI TMDS audio bandwidth constraints. "
+ "EDIDs declare all sample rates (32k-192k); the test "
+ "observes which rates survive in the ELD under "
+ "constrained hblank timings.");
+
+typedef struct {
+ int drm_fd;
+ igt_display_t display;
+ igt_output_t *output;
+ igt_crtc_t *crtc;
+ struct igt_fb fb;
+} data_t;
+
+/* All sample rates declared in the EDID SAD */
+#define ALL_SAMPLE_RATES (CEA_SAD_SAMPLING_RATE_32KHZ | \
+ CEA_SAD_SAMPLING_RATE_44KHZ | \
+ CEA_SAD_SAMPLING_RATE_48KHZ | \
+ CEA_SAD_SAMPLING_RATE_88KHZ | \
+ CEA_SAD_SAMPLING_RATE_96KHZ | \
+ CEA_SAD_SAMPLING_RATE_176KHZ | \
+ CEA_SAD_SAMPLING_RATE_192KHZ)
+
+struct rate_info {
+ unsigned int flag;
+ const char *name;
+ int freq_hz;
+};
+
+static const struct rate_info rate_table[] = {
+ { CEA_SAD_SAMPLING_RATE_32KHZ, "32k", 32000 },
+ { CEA_SAD_SAMPLING_RATE_44KHZ, "44.1k", 44100 },
+ { CEA_SAD_SAMPLING_RATE_48KHZ, "48k", 48000 },
+ { CEA_SAD_SAMPLING_RATE_88KHZ, "88k", 88200 },
+ { CEA_SAD_SAMPLING_RATE_96KHZ, "96k", 96000 },
+ { CEA_SAD_SAMPLING_RATE_176KHZ, "176k", 176400 },
+ { CEA_SAD_SAMPLING_RATE_192KHZ, "192k", 192000 },
+};
+
+#define ACR_RATE_MAX 1500
+#define TOLERANCE_AUDIOCLK_PPM 1000
+#define TOLERANCE_PIXELCLK 0.005
+#define HBLANK_OVERHEAD_STD 30
+#define HBLANK_OVERHEAD_HDCP14 74
+#define DI_PACKET_SIZE 32
+
+static void rates_to_str(unsigned int rates, char *buf, size_t len)
+{
+ int pos = 0;
+
+ buf[0] = '\0';
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ if (!(rates & rate_table[i].flag))
+ continue;
+ if (pos > 0)
+ pos += snprintf(buf + pos, len - pos, ",");
+ pos += snprintf(buf + pos, len - pos, "%s", rate_table[i].name);
+ }
+ if (pos == 0)
+ snprintf(buf, len, "none");
+}
+
+static const int bpc_values[] = { 8, 10, 12 };
+static const int channel_values[] = { 2, 8 };
+
+/*
+ * 1920x1080@60Hz CVT RB2 — hblank=80 (constrained)
+ * Available Packets/Line = FLOOR(((BPC/8)*80 - 74) / 32)
+ * 8bpc=0, 10bpc=0, 12bpc=1
+ */
+static const drmModeModeInfo mode_1080p_hblank80 = {
+ .clock = 133320,
+ .hdisplay = 1920,
+ .hsync_start = 1928,
+ .hsync_end = 1960,
+ .htotal = 2000, /* hblank = 80 */
+ .vdisplay = 1080,
+ .vsync_start = 1097,
+ .vsync_end = 1105,
+ .vtotal = 1111,
+ .vrefresh = 60,
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
+ .type = DRM_MODE_TYPE_DRIVER,
+ .name = "1920x1080",
+};
+
+/*
+ * 1920x1080@60Hz with hblank=160 (relaxed baseline)
+ * Enough hblank for audio at any BPC.
+ */
+static const drmModeModeInfo mode_1080p_hblank160 = {
+ .clock = 148500,
+ .hdisplay = 1920,
+ .hsync_start = 1968,
+ .hsync_end = 2000,
+ .htotal = 2080, /* hblank = 160 */
+ .vdisplay = 1080,
+ .vsync_start = 1097,
+ .vsync_end = 1105,
+ .vtotal = 1111,
+ .vrefresh = 60,
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
+ .type = DRM_MODE_TYPE_DRIVER,
+ .name = "1920x1080",
+};
+
+static igt_output_t *find_hdmi_output(igt_display_t *display)
+{
+ igt_output_t *output;
+
+ for_each_connected_output(display, output) {
+ drmModeConnector *c = output->config.connector;
+
+ if (c->connector_type == DRM_MODE_CONNECTOR_HDMIA ||
+ c->connector_type == DRM_MODE_CONNECTOR_HDMIB)
+ return output;
+ }
+
+ return NULL;
+}
+
+static int hblank_of(const drmModeModeInfo *mode)
+{
+ return mode->htotal - mode->hdisplay;
+}
+
+/*
+ * Driver always reserves HDCP 1.4 rekey overhead (74 clocks) even when
+ * HDCP is not active: 30 (standard) + 44 (HDCP 1.4 rekey quiet period).
+ * FIXME: once driver exposes HDCP state, use 30 for no HDCP, 74 for HDCP 1.4.
+ */
+static int avail_pkts_per_line(int bpc, int hblank)
+{
+ int overhead = HBLANK_OVERHEAD_HDCP14;
+ int tb_blank = (bpc * hblank + 7) / 8; /* CEIL(hblank * bpc/8) */
+ int avail = (tb_blank - overhead) / DI_PACKET_SIZE;
+
+ return avail > 0 ? avail : 0;
+}
+
+/* Packets required per line for a given audio rate and channel layout */
+static int required_pkts_per_line(const drmModeModeInfo *mode, int freq_hz,
+ int channels)
+{
+ double ap = (channels <= 2) ? 0.25 : 1.0;
+ double f_pixel_max = mode->clock * 1000.0 * (1 + TOLERANCE_PIXELCLK);
+ double t_line = mode->htotal / f_pixel_max;
+ double r_ap = ((freq_hz * ap) + (2 * ACR_RATE_MAX)) *
+ (1 + TOLERANCE_AUDIOCLK_PPM / 1e6);
+
+ double avg_pkts = r_ap * t_line;
+
+ return (int)avg_pkts + (avg_pkts > (int)avg_pkts ? 1 : 0);
+}
+
+/*
+ * Build a CEA EDID declaring all 7 sample rates in the SAD.
+ * Deep-color flags in HDMI VSDB match the requested bpc.
+ */
+static const struct edid *
+build_edid(int bpc, int audio_channels)
+{
+ static unsigned char raw_edid[2 * EDID_BLOCK_SIZE];
+ struct edid *edid;
+ struct edid_ext *ext;
+ struct edid_cea *cea;
+ struct edid_cea_data_block *block;
+ struct cea_sad sad;
+ struct hdmi_vsdb hdmi;
+ struct cea_speaker_alloc speakers;
+ size_t offset = 0;
+
+ memset(raw_edid, 0, sizeof(raw_edid));
+ edid = (struct edid *)raw_edid;
+ memcpy(edid, igt_kms_get_base_edid(), sizeof(struct edid));
+ edid->extensions_len = 1;
+
+ ext = &edid->extensions[0];
+ cea = &ext->data.cea;
+
+ if (audio_channels > 0) {
+ cea_sad_init_pcm(&sad,
+ audio_channels,
+ ALL_SAMPLE_RATES,
+ CEA_SAD_SAMPLE_SIZE_16 |
+ CEA_SAD_SAMPLE_SIZE_24);
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_sad(block, &sad, 1);
+ }
+
+ memset(&hdmi, 0, sizeof(hdmi));
+ hdmi.src_phy_addr[0] = 0x10;
+ hdmi.src_phy_addr[1] = 0x00;
+ hdmi.flags1 = HDMI_VSDB_SUPPORTS_AI;
+ hdmi.max_tdms_clock = 340000000 / (5 * 1000000);
+
+ switch (bpc) {
+ case 12:
+ hdmi.flags1 |= HDMI_VSDB_DC_36BIT;
+ /* fall through */
+ case 10:
+ hdmi.flags1 |= HDMI_VSDB_DC_30BIT;
+ /* fall through */
+ case 8:
+ break;
+ }
+
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_hdmi_vsdb(block, &hdmi,
+ sizeof(hdmi));
+
+ memset(&speakers, 0, sizeof(speakers));
+ speakers.speakers = CEA_SPEAKER_FRONT_LEFT_RIGHT;
+ if (audio_channels > 2)
+ speakers.speakers |= CEA_SPEAKER_FRONT_CENTER |
+ CEA_SPEAKER_LFE |
+ CEA_SPEAKER_REAR_LEFT_RIGHT;
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_speaker_alloc(block, &speakers);
+
+ edid_ext_set_cea(ext, offset, 0,
+ EDID_CEA_BASIC_AUDIO | EDID_CEA_UNDERSCAN |
+ EDID_CEA_YCBCR444 | EDID_CEA_YCBCR422);
+ edid_update_checksum(edid);
+
+ return edid;
+}
+
+static void force_edid_and_connector(data_t *data, const struct edid *edid)
+{
+ kmstest_force_edid(data->drm_fd, data->output->config.connector, edid);
+ igt_skip_on_f(!kmstest_force_connector(data->drm_fd,
+ data->output->config.connector,
+ FORCE_CONNECTOR_ON),
+ "Could not force HDMI connector on\n");
+}
+
+static void cleanup_connector(data_t *data)
+{
+ if (data->output->pending_crtc) {
+ igt_plane_t *primary;
+
+ primary = igt_output_get_plane_type(data->output,
+ DRM_PLANE_TYPE_PRIMARY);
+ igt_plane_set_fb(primary, NULL);
+ igt_output_set_crtc(data->output, NULL);
+ igt_display_commit2(&data->display, COMMIT_ATOMIC);
+ }
+
+ igt_remove_fb(data->drm_fd, &data->fb);
+
+ kmstest_force_connector(data->drm_fd,
+ data->output->config.connector,
+ FORCE_CONNECTOR_UNSPECIFIED);
+ kmstest_force_edid(data->drm_fd,
+ data->output->config.connector, NULL);
+}
+
+static int try_modeset(data_t *data, const drmModeModeInfo *mode)
+{
+ igt_plane_t *primary;
+ int ret;
+
+ igt_display_reset(&data->display);
+
+ igt_output_set_crtc(data->output, data->crtc);
+ igt_output_override_mode(data->output, mode);
+
+ primary = igt_output_get_plane_type(data->output,
+ DRM_PLANE_TYPE_PRIMARY);
+
+ igt_create_pattern_fb(data->drm_fd,
+ mode->hdisplay, mode->vdisplay,
+ DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR,
+ &data->fb);
+ igt_plane_set_fb(primary, &data->fb);
+
+ ret = igt_display_try_commit_atomic(&data->display,
+ DRM_MODE_ATOMIC_ALLOW_MODESET,
+ NULL);
+ if (ret) {
+ igt_plane_set_fb(primary, NULL);
+ igt_output_set_crtc(data->output, NULL);
+ igt_remove_fb(data->drm_fd, &data->fb);
+ }
+
+ return ret;
+}
+
+static bool audio_is_active(void)
+{
+ if (!eld_is_supported())
+ return false;
+
+ return eld_has_igt();
+}
+
+static unsigned int get_eld_rates(void)
+{
+ struct eld_entry eld;
+
+ if (!eld_get_igt(&eld))
+ return 0;
+
+ if (eld.sads_len == 0)
+ return 0;
+
+ return eld.sads[0].rates;
+}
+
+static void log_eld_rates(unsigned int declared, unsigned int eld_rates)
+{
+ char decl_str[128], eld_str[128], pruned_str[128];
+ unsigned int pruned = declared & ~eld_rates;
+
+ rates_to_str(declared, decl_str, sizeof(decl_str));
+ rates_to_str(eld_rates, eld_str, sizeof(eld_str));
+ rates_to_str(pruned, pruned_str, sizeof(pruned_str));
+
+ igt_info(" SAD declared: %s\n", decl_str);
+ igt_info(" ELD reports: %s\n", eld_str);
+ if (pruned)
+ igt_info(" Pruned: %s\n", pruned_str);
+}
+
+static void assert_per_rate(const drmModeModeInfo *mode, int channels,
+ int pkts_avail, unsigned int eld_rates)
+{
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
+ channels);
+ bool in_eld = eld_rates & rate_table[i].flag;
+
+ /* A rate that can't fit must not appear in ELD */
+ igt_assert_f(!(req > pkts_avail && in_eld),
+ "%s: req=%d > avail=%d but rate present in ELD\n",
+ rate_table[i].name, req, pkts_avail);
+ }
+}
+
+static void log_per_rate_analysis(const drmModeModeInfo *mode,
+ int bpc, int channels,
+ int pkts_avail, unsigned int eld_rates)
+{
+ const char *layout = (channels <= 2) ? "L0" : "L1";
+
+ igt_info(" %-6s %-3s pkts: avail=%d\n",
+ "Rate", layout, pkts_avail);
+
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
+ channels);
+ const char *expect = (req <= pkts_avail && pkts_avail > 0) ?
+ "fit" : "NO";
+ const char *eld_has = (eld_rates & rate_table[i].flag) ?
+ "yes" : "no";
+
+ igt_info(" %5s: req=%d fit=%s (ELD: %s)\n",
+ rate_table[i].name, req, expect, eld_has);
+ }
+}
+
+/* Run the BPC × channels matrix for a given mode/hblank. */
+static void test_audio_bw_matrix(data_t *data, const drmModeModeInfo *mode)
+{
+ int hblank = hblank_of(mode);
+
+ igt_info("=== Audio BW matrix: %s hblank=%d ===\n",
+ mode->name, hblank);
+
+ for (int b = 0; b < ARRAY_SIZE(bpc_values); b++) {
+ int bpc = bpc_values[b];
+
+ for (int c = 0; c < ARRAY_SIZE(channel_values); c++) {
+ int channels = channel_values[c];
+ const struct edid *edid;
+ int pkts, ret;
+ bool audio;
+ unsigned int eld_rates;
+
+ edid = build_edid(bpc, channels);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output,
+ IGT_CONNECTOR_MAX_BPC, bpc);
+
+ pkts = avail_pkts_per_line(bpc, hblank);
+
+ igt_info("\n %dbpc %dch hblank=%d avail_pkts=%d\n",
+ bpc, channels, hblank, pkts);
+
+ ret = try_modeset(data, mode);
+
+ if (ret) {
+ igt_info(" modeset: REJECTED\n");
+ cleanup_connector(data);
+ continue;
+ }
+
+ /* Allow ELD to propagate */
+ usleep(200 * 1000);
+
+ audio = audio_is_active();
+ eld_rates = audio ? get_eld_rates() : 0;
+
+ igt_info(" modeset: OK\n");
+ igt_info(" audio: %s\n", audio ? "active" : "inactive");
+
+ igt_assert_f(!(pkts == 0 && audio),
+ "Audio active with 0 available packets\n");
+ igt_assert_f(!(pkts > 0 && !audio),
+ "Audio inactive with %d available packets\n",
+ pkts);
+
+ if (audio) {
+ log_eld_rates(ALL_SAMPLE_RATES, eld_rates);
+ assert_per_rate(mode, channels, pkts,
+ eld_rates);
+ }
+
+ log_per_rate_analysis(mode, bpc, channels,
+ pkts, eld_rates);
+
+ cleanup_connector(data);
+ }
+ }
+
+ igt_info("\n=== End matrix ===\n");
+}
+
+static void test_audio_bw_supported(data_t *data)
+{
+ test_audio_bw_matrix(data, &mode_1080p_hblank160);
+}
+
+static void test_audio_bw_pruned(data_t *data)
+{
+ test_audio_bw_matrix(data, &mode_1080p_hblank80);
+}
+
+static void test_suspend_audio_recovery(data_t *data,
+ enum igt_suspend_state state)
+{
+ const struct edid *edid;
+ bool audio_before, audio_after;
+ unsigned int rates_before, rates_after;
+ char before_str[128], after_str[128];
+ int ret;
+
+ edid = build_edid(12, 2);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
+
+ ret = try_modeset(data, &mode_1080p_hblank80);
+ igt_require(ret == 0);
+
+ usleep(200 * 1000);
+
+ audio_before = audio_is_active();
+ rates_before = audio_before ? get_eld_rates() : 0;
+ rates_to_str(rates_before, before_str, sizeof(before_str));
+ igt_info("Before suspend: audio=%d rates=%s\n",
+ audio_before, before_str);
+
+ igt_system_suspend_autoresume(state, SUSPEND_TEST_NONE);
+
+ usleep(200 * 1000);
+
+ audio_after = audio_is_active();
+ rates_after = audio_after ? get_eld_rates() : 0;
+ rates_to_str(rates_after, after_str, sizeof(after_str));
+ igt_info("After suspend: audio=%d rates=%s\n",
+ audio_after, after_str);
+
+ igt_assert_eq(audio_before, audio_after);
+ if (audio_before)
+ igt_assert_eq(rates_before, rates_after);
+
+ cleanup_connector(data);
+}
+
+static void test_runtime_suspend_audio(data_t *data)
+{
+ const struct edid *edid;
+ bool audio_before, audio_after;
+ unsigned int rates_before, rates_after;
+ char before_str[128], after_str[128];
+ int ret;
+
+ edid = build_edid(12, 2);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
+
+ ret = try_modeset(data, &mode_1080p_hblank80);
+ igt_require(ret == 0);
+
+ usleep(200 * 1000);
+
+ audio_before = audio_is_active();
+ rates_before = audio_before ? get_eld_rates() : 0;
+ rates_to_str(rates_before, before_str, sizeof(before_str));
+ igt_info("Before runtime suspend: audio=%d rates=%s\n",
+ audio_before, before_str);
+
+ kmstest_set_connector_dpms(data->drm_fd,
+ data->output->config.connector,
+ DRM_MODE_DPMS_OFF);
+ usleep(500 * 1000);
+ kmstest_set_connector_dpms(data->drm_fd,
+ data->output->config.connector,
+ DRM_MODE_DPMS_ON);
+ usleep(500 * 1000);
+
+ audio_after = audio_is_active();
+ rates_after = audio_after ? get_eld_rates() : 0;
+ rates_to_str(rates_after, after_str, sizeof(after_str));
+ igt_info("After runtime suspend: audio=%d rates=%s\n",
+ audio_after, after_str);
+
+ igt_assert_eq(audio_before, audio_after);
+ if (audio_before)
+ igt_assert_eq(rates_before, rates_after);
+
+ cleanup_connector(data);
+}
+
+int igt_main()
+{
+ data_t data = {};
+
+ igt_fixture() {
+ data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
+ igt_require(is_intel_device(data.drm_fd));
+ kmstest_set_vt_graphics_mode();
+ igt_display_require(&data.display, data.drm_fd);
+
+ data.output = find_hdmi_output(&data.display);
+ igt_require_f(data.output, "No HDMI connector found\n");
+
+ data.crtc = igt_first_crtc(&data.display);
+ igt_require_f(data.crtc, "No usable CRTC found\n");
+ }
+
+ igt_describe("Baseline: hblank=160, audio should be fully supported "
+ "for all BPC and channel configurations.");
+ igt_subtest("audio-bw-supported")
+ test_audio_bw_supported(&data);
+
+ igt_describe("Constrained: hblank=80 (CVT RB2), audio may be pruned "
+ "or disabled depending on BPC.");
+ igt_subtest("audio-bw-pruned")
+ test_audio_bw_pruned(&data);
+
+ igt_describe("Validate audio recovery after S3 suspend with "
+ "constrained hblank.");
+ igt_subtest("suspend-s3-audio-recovery")
+ test_suspend_audio_recovery(&data, SUSPEND_STATE_MEM);
+
+ igt_describe("Validate audio recovery after S4 hibernate with "
+ "constrained hblank.");
+ igt_subtest("suspend-s4-audio-recovery")
+ test_suspend_audio_recovery(&data, SUSPEND_STATE_DISK);
+
+ igt_describe("Validate audio recovery after runtime suspend with "
+ "constrained hblank.");
+ igt_subtest("runtime-suspend-audio-recovery")
+ test_runtime_suspend_audio(&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 a62f447df..facb7ab5d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -259,6 +259,7 @@ intel_kms_progs = [
'kms_fbc_dirty_rect',
'kms_fbcon_fbt',
'kms_fence_pin_leak',
+ 'kms_hdmi_audio_bw',
'kms_flip_scaled_crc',
'kms_flip_tiling',
'kms_frontbuffer_tracking',
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH i-g-t 1/2] tests/intel: Add kms_hdmi_audio_bw test
@ 2026-08-19 7:38 Swati Sharma
2026-08-31 8:55 ` Borah, Chaitanya Kumar
0 siblings, 1 reply; 4+ messages in thread
From: Swati Sharma @ 2026-08-19 7:38 UTC (permalink / raw)
To: igt-dev; +Cc: Swati Sharma
Add a new IGT test to validate HDMI TMDS audio bandwidth constraints
under constrained horizontal blanking intervals.
The test injects EDIDs declaring all 7 CEA sample rates (32kHz-192kHz)
and observes which rates the driver exposes via ELD (EDID-Like Data)
under varying BPC and channel configurations.
Subtests:
- audio-bw-supported: Baseline with hblank=160 where audio bandwidth
is sufficient for all configurations.
- audio-bw-pruned: Constrained hblank=80 (CVT RB2) where the driver
must prune unsustainable sample rates or disable audio entirely.
- suspend-s3/s4-audio-recovery: Verify audio state and sample rates
are preserved across system suspend/resume.
- runtime-suspend-audio-recovery: Verify audio state is preserved
across DPMS off/on cycles.
The test uses the HDMI TMDS bandwidth formula from the spec:
pkts_avail = FLOOR((CEIL(hblank * bpc/8) - overhead) / 32)
pkts_reqd = CEIL(R_AP * T_line)
where overhead=74 (HDCP 1.4 rekey always reserved by the driver).
Assertions verify:
- Audio is active when bandwidth is available (pkts_avail > 0)
- Audio is inactive when bandwidth is exhausted (pkts_avail == 0)
- No sample rate requiring more packets than available appears in ELD
Signed-off-by: Swati Sharma <swati2.sharma@intel.com>
Assisted-by: GitHub Copilot:Claude Opus 4.6
---
tests/intel/kms_hdmi_audio_bw.c | 626 ++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 627 insertions(+)
create mode 100644 tests/intel/kms_hdmi_audio_bw.c
diff --git a/tests/intel/kms_hdmi_audio_bw.c b/tests/intel/kms_hdmi_audio_bw.c
new file mode 100644
index 000000000..4b6f0d001
--- /dev/null
+++ b/tests/intel/kms_hdmi_audio_bw.c
@@ -0,0 +1,626 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * TEST: kms hdmi audio bw
+ * Category: Display
+ * Description: Validate HDMI TMDS audio bandwidth constraints by injecting
+ * EDIDs with all sample rates declared and observing which rates
+ * the driver exposes (via ELD) under varying BPC / channel /
+ * hblank configurations.
+ * Driver requirement: i915, xe
+ * Mega feature: Display Audio
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <string.h>
+
+#include "igt.h"
+#include "igt_edid.h"
+#include "igt_eld.h"
+#include "igt_aux.h"
+#include "xe/xe_query.h"
+
+/**
+ * SUBTEST: audio-bw-supported
+ * Description: Baseline test with hblank=160 where audio bandwidth is
+ * sufficient for all BPC and channel combinations. Verifies
+ * that no sample rates are pruned.
+ *
+ * SUBTEST: audio-bw-pruned
+ * Description: Constrained test with hblank=80 (CVT RB2) where audio
+ * bandwidth is limited. Logs which sample rates are pruned
+ * per BPC and channel combination.
+ *
+ * SUBTEST: suspend-%s-audio-recovery
+ * Description: Validate audio state restoration after %arg[1] with
+ * constrained hblank=80 and 12bpc.
+ *
+ * arg[1]:
+ *
+ * @s3: S3 (suspend to RAM)
+ * @s4: S4 (hibernate)
+ *
+ * SUBTEST: runtime-suspend-audio-recovery
+ * Description: Validate audio state restoration after runtime suspend/resume
+ * with constrained hblank=80 and 12bpc.
+ */
+
+IGT_TEST_DESCRIPTION("Validate HDMI TMDS audio bandwidth constraints. "
+ "EDIDs declare all sample rates (32k-192k); the test "
+ "observes which rates survive in the ELD under "
+ "constrained hblank timings.");
+
+typedef struct {
+ int drm_fd;
+ igt_display_t display;
+ igt_output_t *output;
+ igt_crtc_t *crtc;
+ struct igt_fb fb;
+} data_t;
+
+/* All sample rates declared in the EDID SAD */
+#define ALL_SAMPLE_RATES (CEA_SAD_SAMPLING_RATE_32KHZ | \
+ CEA_SAD_SAMPLING_RATE_44KHZ | \
+ CEA_SAD_SAMPLING_RATE_48KHZ | \
+ CEA_SAD_SAMPLING_RATE_88KHZ | \
+ CEA_SAD_SAMPLING_RATE_96KHZ | \
+ CEA_SAD_SAMPLING_RATE_176KHZ | \
+ CEA_SAD_SAMPLING_RATE_192KHZ)
+
+struct rate_info {
+ unsigned int flag;
+ const char *name;
+ int freq_hz;
+};
+
+static const struct rate_info rate_table[] = {
+ { CEA_SAD_SAMPLING_RATE_32KHZ, "32k", 32000 },
+ { CEA_SAD_SAMPLING_RATE_44KHZ, "44.1k", 44100 },
+ { CEA_SAD_SAMPLING_RATE_48KHZ, "48k", 48000 },
+ { CEA_SAD_SAMPLING_RATE_88KHZ, "88k", 88200 },
+ { CEA_SAD_SAMPLING_RATE_96KHZ, "96k", 96000 },
+ { CEA_SAD_SAMPLING_RATE_176KHZ, "176k", 176400 },
+ { CEA_SAD_SAMPLING_RATE_192KHZ, "192k", 192000 },
+};
+
+#define ACR_RATE_MAX 1500
+#define TOLERANCE_AUDIOCLK_PPM 1000
+#define TOLERANCE_PIXELCLK 0.005
+#define HBLANK_OVERHEAD_STD 30
+#define HBLANK_OVERHEAD_HDCP14 74
+#define DI_PACKET_SIZE 32
+
+static void rates_to_str(unsigned int rates, char *buf, size_t len)
+{
+ int pos = 0;
+
+ buf[0] = '\0';
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ if (!(rates & rate_table[i].flag))
+ continue;
+ if (pos > 0)
+ pos += snprintf(buf + pos, len - pos, ",");
+ pos += snprintf(buf + pos, len - pos, "%s", rate_table[i].name);
+ }
+ if (pos == 0)
+ snprintf(buf, len, "none");
+}
+
+static const int bpc_values[] = { 8, 10, 12 };
+static const int channel_values[] = { 2, 8 };
+
+/*
+ * 1920x1080@60Hz CVT RB2 — hblank=80 (constrained)
+ * Available Packets/Line = FLOOR(((BPC/8)*80 - 74) / 32)
+ * 8bpc=0, 10bpc=0, 12bpc=1
+ */
+static const drmModeModeInfo mode_1080p_hblank80 = {
+ .clock = 133320,
+ .hdisplay = 1920,
+ .hsync_start = 1928,
+ .hsync_end = 1960,
+ .htotal = 2000, /* hblank = 80 */
+ .vdisplay = 1080,
+ .vsync_start = 1097,
+ .vsync_end = 1105,
+ .vtotal = 1111,
+ .vrefresh = 60,
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
+ .type = DRM_MODE_TYPE_DRIVER,
+ .name = "1920x1080",
+};
+
+/*
+ * 1920x1080@60Hz with hblank=160 (relaxed baseline)
+ * Enough hblank for audio at any BPC.
+ */
+static const drmModeModeInfo mode_1080p_hblank160 = {
+ .clock = 148500,
+ .hdisplay = 1920,
+ .hsync_start = 1968,
+ .hsync_end = 2000,
+ .htotal = 2080, /* hblank = 160 */
+ .vdisplay = 1080,
+ .vsync_start = 1097,
+ .vsync_end = 1105,
+ .vtotal = 1111,
+ .vrefresh = 60,
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
+ .type = DRM_MODE_TYPE_DRIVER,
+ .name = "1920x1080",
+};
+
+static igt_output_t *find_hdmi_output(igt_display_t *display)
+{
+ igt_output_t *output;
+
+ for_each_connected_output(display, output) {
+ drmModeConnector *c = output->config.connector;
+
+ if (c->connector_type == DRM_MODE_CONNECTOR_HDMIA ||
+ c->connector_type == DRM_MODE_CONNECTOR_HDMIB)
+ return output;
+ }
+
+ return NULL;
+}
+
+static int hblank_of(const drmModeModeInfo *mode)
+{
+ return mode->htotal - mode->hdisplay;
+}
+
+/*
+ * Driver always reserves HDCP 1.4 rekey overhead (74 clocks) even when
+ * HDCP is not active: 30 (standard) + 44 (HDCP 1.4 rekey quiet period).
+ * FIXME: once driver exposes HDCP state, use 30 for no HDCP, 74 for HDCP 1.4.
+ */
+static int avail_pkts_per_line(int bpc, int hblank)
+{
+ int overhead = HBLANK_OVERHEAD_HDCP14;
+ int tb_blank = (bpc * hblank + 7) / 8; /* CEIL(hblank * bpc/8) */
+ int avail = (tb_blank - overhead) / DI_PACKET_SIZE;
+
+ return avail > 0 ? avail : 0;
+}
+
+/* Packets required per line for a given audio rate and channel layout */
+static int required_pkts_per_line(const drmModeModeInfo *mode, int freq_hz,
+ int channels)
+{
+ double ap = (channels <= 2) ? 0.25 : 1.0;
+ double f_pixel_max = mode->clock * 1000.0 * (1 + TOLERANCE_PIXELCLK);
+ double t_line = mode->htotal / f_pixel_max;
+ double r_ap = ((freq_hz * ap) + (2 * ACR_RATE_MAX)) *
+ (1 + TOLERANCE_AUDIOCLK_PPM / 1e6);
+
+ double avg_pkts = r_ap * t_line;
+
+ return (int)avg_pkts + (avg_pkts > (int)avg_pkts ? 1 : 0);
+}
+
+/*
+ * Build a CEA EDID declaring all 7 sample rates in the SAD.
+ * Deep-color flags in HDMI VSDB match the requested bpc.
+ */
+static const struct edid *
+build_edid(int bpc, int audio_channels)
+{
+ static unsigned char raw_edid[2 * EDID_BLOCK_SIZE];
+ struct edid *edid;
+ struct edid_ext *ext;
+ struct edid_cea *cea;
+ struct edid_cea_data_block *block;
+ struct cea_sad sad;
+ struct hdmi_vsdb hdmi;
+ struct cea_speaker_alloc speakers;
+ size_t offset = 0;
+
+ memset(raw_edid, 0, sizeof(raw_edid));
+ edid = (struct edid *)raw_edid;
+ memcpy(edid, igt_kms_get_base_edid(), sizeof(struct edid));
+ edid->extensions_len = 1;
+
+ ext = &edid->extensions[0];
+ cea = &ext->data.cea;
+
+ if (audio_channels > 0) {
+ cea_sad_init_pcm(&sad,
+ audio_channels,
+ ALL_SAMPLE_RATES,
+ CEA_SAD_SAMPLE_SIZE_16 |
+ CEA_SAD_SAMPLE_SIZE_24);
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_sad(block, &sad, 1);
+ }
+
+ memset(&hdmi, 0, sizeof(hdmi));
+ hdmi.src_phy_addr[0] = 0x10;
+ hdmi.src_phy_addr[1] = 0x00;
+ hdmi.flags1 = HDMI_VSDB_SUPPORTS_AI;
+ hdmi.max_tdms_clock = 340000000 / (5 * 1000000);
+
+ switch (bpc) {
+ case 12:
+ hdmi.flags1 |= HDMI_VSDB_DC_36BIT;
+ /* fall through */
+ case 10:
+ hdmi.flags1 |= HDMI_VSDB_DC_30BIT;
+ /* fall through */
+ case 8:
+ break;
+ }
+
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_hdmi_vsdb(block, &hdmi,
+ sizeof(hdmi));
+
+ memset(&speakers, 0, sizeof(speakers));
+ speakers.speakers = CEA_SPEAKER_FRONT_LEFT_RIGHT;
+ if (audio_channels > 2)
+ speakers.speakers |= CEA_SPEAKER_FRONT_CENTER |
+ CEA_SPEAKER_LFE |
+ CEA_SPEAKER_REAR_LEFT_RIGHT;
+ block = (struct edid_cea_data_block *)&cea->data[offset];
+ offset += edid_cea_data_block_set_speaker_alloc(block, &speakers);
+
+ edid_ext_set_cea(ext, offset, 0,
+ EDID_CEA_BASIC_AUDIO | EDID_CEA_UNDERSCAN |
+ EDID_CEA_YCBCR444 | EDID_CEA_YCBCR422);
+ edid_update_checksum(edid);
+
+ return edid;
+}
+
+static void force_edid_and_connector(data_t *data, const struct edid *edid)
+{
+ kmstest_force_edid(data->drm_fd, data->output->config.connector, edid);
+ igt_skip_on_f(!kmstest_force_connector(data->drm_fd,
+ data->output->config.connector,
+ FORCE_CONNECTOR_ON),
+ "Could not force HDMI connector on\n");
+}
+
+static void cleanup_connector(data_t *data)
+{
+ if (data->output->pending_crtc) {
+ igt_plane_t *primary;
+
+ primary = igt_output_get_plane_type(data->output,
+ DRM_PLANE_TYPE_PRIMARY);
+ igt_plane_set_fb(primary, NULL);
+ igt_output_set_crtc(data->output, NULL);
+ igt_display_commit2(&data->display, COMMIT_ATOMIC);
+ }
+
+ igt_remove_fb(data->drm_fd, &data->fb);
+
+ kmstest_force_connector(data->drm_fd,
+ data->output->config.connector,
+ FORCE_CONNECTOR_UNSPECIFIED);
+ kmstest_force_edid(data->drm_fd,
+ data->output->config.connector, NULL);
+}
+
+static int try_modeset(data_t *data, const drmModeModeInfo *mode)
+{
+ igt_plane_t *primary;
+ int ret;
+
+ igt_display_reset(&data->display);
+
+ igt_output_set_crtc(data->output, data->crtc);
+ igt_output_override_mode(data->output, mode);
+
+ primary = igt_output_get_plane_type(data->output,
+ DRM_PLANE_TYPE_PRIMARY);
+
+ igt_create_pattern_fb(data->drm_fd,
+ mode->hdisplay, mode->vdisplay,
+ DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR,
+ &data->fb);
+ igt_plane_set_fb(primary, &data->fb);
+
+ ret = igt_display_try_commit_atomic(&data->display,
+ DRM_MODE_ATOMIC_ALLOW_MODESET,
+ NULL);
+ if (ret) {
+ igt_plane_set_fb(primary, NULL);
+ igt_output_set_crtc(data->output, NULL);
+ igt_remove_fb(data->drm_fd, &data->fb);
+ }
+
+ return ret;
+}
+
+static bool audio_is_active(void)
+{
+ if (!eld_is_supported())
+ return false;
+
+ return eld_has_igt();
+}
+
+static unsigned int get_eld_rates(void)
+{
+ struct eld_entry eld;
+
+ if (!eld_get_igt(&eld))
+ return 0;
+
+ if (eld.sads_len == 0)
+ return 0;
+
+ return eld.sads[0].rates;
+}
+
+static void log_eld_rates(unsigned int declared, unsigned int eld_rates)
+{
+ char decl_str[128], eld_str[128], pruned_str[128];
+ unsigned int pruned = declared & ~eld_rates;
+
+ rates_to_str(declared, decl_str, sizeof(decl_str));
+ rates_to_str(eld_rates, eld_str, sizeof(eld_str));
+ rates_to_str(pruned, pruned_str, sizeof(pruned_str));
+
+ igt_info(" SAD declared: %s\n", decl_str);
+ igt_info(" ELD reports: %s\n", eld_str);
+ if (pruned)
+ igt_info(" Pruned: %s\n", pruned_str);
+}
+
+static void assert_per_rate(const drmModeModeInfo *mode, int channels,
+ int pkts_avail, unsigned int eld_rates)
+{
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
+ channels);
+ bool in_eld = eld_rates & rate_table[i].flag;
+
+ /* A rate that can't fit must not appear in ELD */
+ igt_assert_f(!(req > pkts_avail && in_eld),
+ "%s: req=%d > avail=%d but rate present in ELD\n",
+ rate_table[i].name, req, pkts_avail);
+ }
+}
+
+static void log_per_rate_analysis(const drmModeModeInfo *mode,
+ int bpc, int channels,
+ int pkts_avail, unsigned int eld_rates)
+{
+ const char *layout = (channels <= 2) ? "L0" : "L1";
+
+ igt_info(" %-6s %-3s pkts: avail=%d\n",
+ "Rate", layout, pkts_avail);
+
+ for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
+ int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
+ channels);
+ const char *expect = (req <= pkts_avail && pkts_avail > 0) ?
+ "fit" : "NO";
+ const char *eld_has = (eld_rates & rate_table[i].flag) ?
+ "yes" : "no";
+
+ igt_info(" %5s: req=%d fit=%s (ELD: %s)\n",
+ rate_table[i].name, req, expect, eld_has);
+ }
+}
+
+/* Run the BPC × channels matrix for a given mode/hblank. */
+static void test_audio_bw_matrix(data_t *data, const drmModeModeInfo *mode)
+{
+ int hblank = hblank_of(mode);
+
+ igt_info("=== Audio BW matrix: %s hblank=%d ===\n",
+ mode->name, hblank);
+
+ for (int b = 0; b < ARRAY_SIZE(bpc_values); b++) {
+ int bpc = bpc_values[b];
+
+ for (int c = 0; c < ARRAY_SIZE(channel_values); c++) {
+ int channels = channel_values[c];
+ const struct edid *edid;
+ int pkts, ret;
+ bool audio;
+ unsigned int eld_rates;
+
+ edid = build_edid(bpc, channels);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output,
+ IGT_CONNECTOR_MAX_BPC, bpc);
+
+ pkts = avail_pkts_per_line(bpc, hblank);
+
+ igt_info("\n %dbpc %dch hblank=%d avail_pkts=%d\n",
+ bpc, channels, hblank, pkts);
+
+ ret = try_modeset(data, mode);
+
+ if (ret) {
+ igt_info(" modeset: REJECTED\n");
+ cleanup_connector(data);
+ continue;
+ }
+
+ /* Allow ELD to propagate */
+ usleep(200 * 1000);
+
+ audio = audio_is_active();
+ eld_rates = audio ? get_eld_rates() : 0;
+
+ igt_info(" modeset: OK\n");
+ igt_info(" audio: %s\n", audio ? "active" : "inactive");
+
+ igt_assert_f(!(pkts == 0 && audio),
+ "Audio active with 0 available packets\n");
+ igt_assert_f(!(pkts > 0 && !audio),
+ "Audio inactive with %d available packets\n",
+ pkts);
+
+ if (audio) {
+ log_eld_rates(ALL_SAMPLE_RATES, eld_rates);
+ assert_per_rate(mode, channels, pkts,
+ eld_rates);
+ }
+
+ log_per_rate_analysis(mode, bpc, channels,
+ pkts, eld_rates);
+
+ cleanup_connector(data);
+ }
+ }
+
+ igt_info("\n=== End matrix ===\n");
+}
+
+static void test_audio_bw_supported(data_t *data)
+{
+ test_audio_bw_matrix(data, &mode_1080p_hblank160);
+}
+
+static void test_audio_bw_pruned(data_t *data)
+{
+ test_audio_bw_matrix(data, &mode_1080p_hblank80);
+}
+
+static void test_suspend_audio_recovery(data_t *data,
+ enum igt_suspend_state state)
+{
+ const struct edid *edid;
+ bool audio_before, audio_after;
+ unsigned int rates_before, rates_after;
+ char before_str[128], after_str[128];
+ int ret;
+
+ edid = build_edid(12, 2);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
+
+ ret = try_modeset(data, &mode_1080p_hblank80);
+ igt_require(ret == 0);
+
+ usleep(200 * 1000);
+
+ audio_before = audio_is_active();
+ rates_before = audio_before ? get_eld_rates() : 0;
+ rates_to_str(rates_before, before_str, sizeof(before_str));
+ igt_info("Before suspend: audio=%d rates=%s\n",
+ audio_before, before_str);
+
+ igt_system_suspend_autoresume(state, SUSPEND_TEST_NONE);
+
+ usleep(200 * 1000);
+
+ audio_after = audio_is_active();
+ rates_after = audio_after ? get_eld_rates() : 0;
+ rates_to_str(rates_after, after_str, sizeof(after_str));
+ igt_info("After suspend: audio=%d rates=%s\n",
+ audio_after, after_str);
+
+ igt_assert_eq(audio_before, audio_after);
+ if (audio_before)
+ igt_assert_eq(rates_before, rates_after);
+
+ cleanup_connector(data);
+}
+
+static void test_runtime_suspend_audio(data_t *data)
+{
+ const struct edid *edid;
+ bool audio_before, audio_after;
+ unsigned int rates_before, rates_after;
+ char before_str[128], after_str[128];
+ int ret;
+
+ edid = build_edid(12, 2);
+ force_edid_and_connector(data, edid);
+
+ igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
+
+ ret = try_modeset(data, &mode_1080p_hblank80);
+ igt_require(ret == 0);
+
+ usleep(200 * 1000);
+
+ audio_before = audio_is_active();
+ rates_before = audio_before ? get_eld_rates() : 0;
+ rates_to_str(rates_before, before_str, sizeof(before_str));
+ igt_info("Before runtime suspend: audio=%d rates=%s\n",
+ audio_before, before_str);
+
+ kmstest_set_connector_dpms(data->drm_fd,
+ data->output->config.connector,
+ DRM_MODE_DPMS_OFF);
+ usleep(500 * 1000);
+ kmstest_set_connector_dpms(data->drm_fd,
+ data->output->config.connector,
+ DRM_MODE_DPMS_ON);
+ usleep(500 * 1000);
+
+ audio_after = audio_is_active();
+ rates_after = audio_after ? get_eld_rates() : 0;
+ rates_to_str(rates_after, after_str, sizeof(after_str));
+ igt_info("After runtime suspend: audio=%d rates=%s\n",
+ audio_after, after_str);
+
+ igt_assert_eq(audio_before, audio_after);
+ if (audio_before)
+ igt_assert_eq(rates_before, rates_after);
+
+ cleanup_connector(data);
+}
+
+int igt_main()
+{
+ data_t data = {};
+
+ igt_fixture() {
+ data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
+ igt_require(is_intel_device(data.drm_fd));
+ kmstest_set_vt_graphics_mode();
+ igt_display_require(&data.display, data.drm_fd);
+
+ data.output = find_hdmi_output(&data.display);
+ igt_require_f(data.output, "No HDMI connector found\n");
+
+ data.crtc = igt_first_crtc(&data.display);
+ igt_require_f(data.crtc, "No usable CRTC found\n");
+ }
+
+ igt_describe("Baseline: hblank=160, audio should be fully supported "
+ "for all BPC and channel configurations.");
+ igt_subtest("audio-bw-supported")
+ test_audio_bw_supported(&data);
+
+ igt_describe("Constrained: hblank=80 (CVT RB2), audio may be pruned "
+ "or disabled depending on BPC.");
+ igt_subtest("audio-bw-pruned")
+ test_audio_bw_pruned(&data);
+
+ igt_describe("Validate audio recovery after S3 suspend with "
+ "constrained hblank.");
+ igt_subtest("suspend-s3-audio-recovery")
+ test_suspend_audio_recovery(&data, SUSPEND_STATE_MEM);
+
+ igt_describe("Validate audio recovery after S4 hibernate with "
+ "constrained hblank.");
+ igt_subtest("suspend-s4-audio-recovery")
+ test_suspend_audio_recovery(&data, SUSPEND_STATE_DISK);
+
+ igt_describe("Validate audio recovery after runtime suspend with "
+ "constrained hblank.");
+ igt_subtest("runtime-suspend-audio-recovery")
+ test_runtime_suspend_audio(&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 a62f447df..facb7ab5d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -259,6 +259,7 @@ intel_kms_progs = [
'kms_fbc_dirty_rect',
'kms_fbcon_fbt',
'kms_fence_pin_leak',
+ 'kms_hdmi_audio_bw',
'kms_flip_scaled_crc',
'kms_flip_tiling',
'kms_frontbuffer_tracking',
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH i-g-t 1/2] tests/intel: Add kms_hdmi_audio_bw test
2026-08-19 7:38 Swati Sharma
@ 2026-08-31 8:55 ` Borah, Chaitanya Kumar
2026-09-01 12:03 ` Borah, Chaitanya Kumar
0 siblings, 1 reply; 4+ messages in thread
From: Borah, Chaitanya Kumar @ 2026-08-31 8:55 UTC (permalink / raw)
To: Swati Sharma, igt-dev
On 8/19/2026 1:08 PM, Swati Sharma wrote:
> Add a new IGT test to validate HDMI TMDS audio bandwidth constraints
> under constrained horizontal blanking intervals.
>
> The test injects EDIDs declaring all 7 CEA sample rates (32kHz-192kHz)
> and observes which rates the driver exposes via ELD (EDID-Like Data)
> under varying BPC and channel configurations.
>
> Subtests:
> - audio-bw-supported: Baseline with hblank=160 where audio bandwidth
> is sufficient for all configurations.
> - audio-bw-pruned: Constrained hblank=80 (CVT RB2) where the driver
> must prune unsustainable sample rates or disable audio entirely.
> - suspend-s3/s4-audio-recovery: Verify audio state and sample rates
> are preserved across system suspend/resume.
> - runtime-suspend-audio-recovery: Verify audio state is preserved
> across DPMS off/on cycles.
>
> The test uses the HDMI TMDS bandwidth formula from the spec:
> pkts_avail = FLOOR((CEIL(hblank * bpc/8) - overhead) / 32)
> pkts_reqd = CEIL(R_AP * T_line)
> where overhead=74 (HDCP 1.4 rekey always reserved by the driver).
>
> Assertions verify:
> - Audio is active when bandwidth is available (pkts_avail > 0)
> - Audio is inactive when bandwidth is exhausted (pkts_avail == 0)
> - No sample rate requiring more packets than available appears in ELD
>
> Signed-off-by: Swati Sharma <swati2.sharma@intel.com>
> Assisted-by: GitHub Copilot:Claude Opus 4.6
> ---
> tests/intel/kms_hdmi_audio_bw.c | 626 ++++++++++++++++++++++++++++++++
> tests/meson.build | 1 +
> 2 files changed, 627 insertions(+)
> create mode 100644 tests/intel/kms_hdmi_audio_bw.c
>
> diff --git a/tests/intel/kms_hdmi_audio_bw.c b/tests/intel/kms_hdmi_audio_bw.c
> new file mode 100644
> index 000000000..4b6f0d001
> --- /dev/null
> +++ b/tests/intel/kms_hdmi_audio_bw.c
> @@ -0,0 +1,626 @@
> +// SPDX-License-Identifier: MIT
> +/*
> + * Copyright © 2026 Intel Corporation
> + */
> +
> +/**
> + * TEST: kms hdmi audio bw
> + * Category: Display
> + * Description: Validate HDMI TMDS audio bandwidth constraints by injecting
> + * EDIDs with all sample rates declared and observing which rates
> + * the driver exposes (via ELD) under varying BPC / channel /
> + * hblank configurations.
> + * Driver requirement: i915, xe
> + * Mega feature: Display Audio
> + */
> +
> +#include "config.h"
> +
> +#include <math.h>
> +#include <string.h>
> +
> +#include "igt.h"
> +#include "igt_edid.h"
> +#include "igt_eld.h"
> +#include "igt_aux.h"
> +#include "xe/xe_query.h"
What is this used for?
> +
> +/**
> + * SUBTEST: audio-bw-supported
> + * Description: Baseline test with hblank=160 where audio bandwidth is
> + * sufficient for all BPC and channel combinations. Verifies
> + * that no sample rates are pruned.
> + *
> + * SUBTEST: audio-bw-pruned
> + * Description: Constrained test with hblank=80 (CVT RB2) where audio
> + * bandwidth is limited. Logs which sample rates are pruned
> + * per BPC and channel combination.
> + *
> + * SUBTEST: suspend-%s-audio-recovery
> + * Description: Validate audio state restoration after %arg[1] with
> + * constrained hblank=80 and 12bpc.
> + *
> + * arg[1]:
> + *
> + * @s3: S3 (suspend to RAM)
> + * @s4: S4 (hibernate)
> + *
> + * SUBTEST: runtime-suspend-audio-recovery
> + * Description: Validate audio state restoration after runtime suspend/resume
> + * with constrained hblank=80 and 12bpc.
> + */
> +
> +IGT_TEST_DESCRIPTION("Validate HDMI TMDS audio bandwidth constraints. "
> + "EDIDs declare all sample rates (32k-192k); the test "
> + "observes which rates survive in the ELD under "
> + "constrained hblank timings.");
> +
> +typedef struct {
> + int drm_fd;
> + igt_display_t display;
> + igt_output_t *output;
> + igt_crtc_t *crtc;
> + struct igt_fb fb;
> +} data_t;
> +
> +/* All sample rates declared in the EDID SAD */
> +#define ALL_SAMPLE_RATES (CEA_SAD_SAMPLING_RATE_32KHZ | \
> + CEA_SAD_SAMPLING_RATE_44KHZ | \
> + CEA_SAD_SAMPLING_RATE_48KHZ | \
> + CEA_SAD_SAMPLING_RATE_88KHZ | \
> + CEA_SAD_SAMPLING_RATE_96KHZ | \
> + CEA_SAD_SAMPLING_RATE_176KHZ | \
> + CEA_SAD_SAMPLING_RATE_192KHZ)
> +
> +struct rate_info {
> + unsigned int flag;
> + const char *name;
> + int freq_hz;
> +};
> +
> +static const struct rate_info rate_table[] = {
> + { CEA_SAD_SAMPLING_RATE_32KHZ, "32k", 32000 },
> + { CEA_SAD_SAMPLING_RATE_44KHZ, "44.1k", 44100 },
> + { CEA_SAD_SAMPLING_RATE_48KHZ, "48k", 48000 },
> + { CEA_SAD_SAMPLING_RATE_88KHZ, "88k", 88200 },
> + { CEA_SAD_SAMPLING_RATE_96KHZ, "96k", 96000 },
> + { CEA_SAD_SAMPLING_RATE_176KHZ, "176k", 176400 },
> + { CEA_SAD_SAMPLING_RATE_192KHZ, "192k", 192000 },
> +};
> +
> +#define ACR_RATE_MAX 1500
> +#define TOLERANCE_AUDIOCLK_PPM 1000
> +#define TOLERANCE_PIXELCLK 0.005
> +#define HBLANK_OVERHEAD_STD 30
> +#define HBLANK_OVERHEAD_HDCP14 74
> +#define DI_PACKET_SIZE 32
> +
> +static void rates_to_str(unsigned int rates, char *buf, size_t len)
> +{
> + int pos = 0;
> +
> + buf[0] = '\0';
> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
> + if (!(rates & rate_table[i].flag))
> + continue;
> + if (pos > 0)
> + pos += snprintf(buf + pos, len - pos, ",");
> + pos += snprintf(buf + pos, len - pos, "%s", rate_table[i].name);
> + }
> + if (pos == 0)
> + snprintf(buf, len, "none");
> +}
> +
> +static const int bpc_values[] = { 8, 10, 12 };
> +static const int channel_values[] = { 2, 8 };
> +
> +/*
> + * 1920x1080@60Hz CVT RB2 — hblank=80 (constrained)
> + * Available Packets/Line = FLOOR(((BPC/8)*80 - 74) / 32)
> + * 8bpc=0, 10bpc=0, 12bpc=1
> + */
> +static const drmModeModeInfo mode_1080p_hblank80 = {
> + .clock = 133320,
> + .hdisplay = 1920,
> + .hsync_start = 1928,
> + .hsync_end = 1960,
> + .htotal = 2000, /* hblank = 80 */
> + .vdisplay = 1080,
> + .vsync_start = 1097,
> + .vsync_end = 1105,
> + .vtotal = 1111,
> + .vrefresh = 60,
> + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
> + .type = DRM_MODE_TYPE_DRIVER,
> + .name = "1920x1080",
> +};
> +
> +/*
> + * 1920x1080@60Hz with hblank=160 (relaxed baseline)
> + * Enough hblank for audio at any BPC.
> + */
> +static const drmModeModeInfo mode_1080p_hblank160 = {
> + .clock = 148500,
> + .hdisplay = 1920,
> + .hsync_start = 1968,
> + .hsync_end = 2000,
> + .htotal = 2080, /* hblank = 160 */
> + .vdisplay = 1080,
> + .vsync_start = 1097,
> + .vsync_end = 1105,
> + .vtotal = 1111,
> + .vrefresh = 60,
> + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
> + .type = DRM_MODE_TYPE_DRIVER,
> + .name = "1920x1080",
> +};
> +
> +static igt_output_t *find_hdmi_output(igt_display_t *display)
> +{
> + igt_output_t *output;
> +
> + for_each_connected_output(display, output) {
> + drmModeConnector *c = output->config.connector;
> +
> + if (c->connector_type == DRM_MODE_CONNECTOR_HDMIA ||
> + c->connector_type == DRM_MODE_CONNECTOR_HDMIB)
> + return output;
> + }
> +
> + return NULL;
> +}
> +
> +static int hblank_of(const drmModeModeInfo *mode)
> +{
> + return mode->htotal - mode->hdisplay;
> +}
> +
> +/*
> + * Driver always reserves HDCP 1.4 rekey overhead (74 clocks) even when
> + * HDCP is not active: 30 (standard) + 44 (HDCP 1.4 rekey quiet period).
> + * FIXME: once driver exposes HDCP state, use 30 for no HDCP, 74 for HDCP 1.4.
> + */
> +static int avail_pkts_per_line(int bpc, int hblank)
> +{
> + int overhead = HBLANK_OVERHEAD_HDCP14;
> + int tb_blank = (bpc * hblank + 7) / 8; /* CEIL(hblank * bpc/8) */
> + int avail = (tb_blank - overhead) / DI_PACKET_SIZE;
> +
> + return avail > 0 ? avail : 0;
> +}
> +
> +/* Packets required per line for a given audio rate and channel layout */
> +static int required_pkts_per_line(const drmModeModeInfo *mode, int freq_hz,
> + int channels)
> +{
> + double ap = (channels <= 2) ? 0.25 : 1.0;
> + double f_pixel_max = mode->clock * 1000.0 * (1 + TOLERANCE_PIXELCLK);
> + double t_line = mode->htotal / f_pixel_max;
> + double r_ap = ((freq_hz * ap) + (2 * ACR_RATE_MAX)) *
> + (1 + TOLERANCE_AUDIOCLK_PPM / 1e6);
> +
> + double avg_pkts = r_ap * t_line;
> +
> + return (int)avg_pkts + (avg_pkts > (int)avg_pkts ? 1 : 0);
> +}
> +
> +/*
> + * Build a CEA EDID declaring all 7 sample rates in the SAD.
> + * Deep-color flags in HDMI VSDB match the requested bpc.
> + */
> +static const struct edid *
> +build_edid(int bpc, int audio_channels)
> +{
> + static unsigned char raw_edid[2 * EDID_BLOCK_SIZE];
> + struct edid *edid;
> + struct edid_ext *ext;
> + struct edid_cea *cea;
> + struct edid_cea_data_block *block;
> + struct cea_sad sad;
> + struct hdmi_vsdb hdmi;
nit: Let's call it vsdb.
> + struct cea_speaker_alloc speakers;
> + size_t offset = 0;
> +
> + memset(raw_edid, 0, sizeof(raw_edid));
> + edid = (struct edid *)raw_edid;
> + memcpy(edid, igt_kms_get_base_edid(), sizeof(struct edid));
> + edid->extensions_len = 1;
> +
> + ext = &edid->extensions[0];
> + cea = &ext->data.cea;
> +
> + if (audio_channels > 0) {
> + cea_sad_init_pcm(&sad,
> + audio_channels,
> + ALL_SAMPLE_RATES,
> + CEA_SAD_SAMPLE_SIZE_16 |
> + CEA_SAD_SAMPLE_SIZE_24);
> + block = (struct edid_cea_data_block *)&cea->data[offset];
> + offset += edid_cea_data_block_set_sad(block, &sad, 1);
> + }
> +
> + memset(&hdmi, 0, sizeof(hdmi));
> + hdmi.src_phy_addr[0] = 0x10;
> + hdmi.src_phy_addr[1] = 0x00;
> + hdmi.flags1 = HDMI_VSDB_SUPPORTS_AI;
> + hdmi.max_tdms_clock = 340000000 / (5 * 1000000);
where do these number come from?
> +
> + switch (bpc) {
> + case 12:
> + hdmi.flags1 |= HDMI_VSDB_DC_36BIT;
> + /* fall through */
> + case 10:
> + hdmi.flags1 |= HDMI_VSDB_DC_30BIT;
> + /* fall through */
> + case 8:
> + break;
> + }
> +
> + block = (struct edid_cea_data_block *)&cea->data[offset];
> + offset += edid_cea_data_block_set_hdmi_vsdb(block, &hdmi,
> + sizeof(hdmi));
> +
> + memset(&speakers, 0, sizeof(speakers));
> + speakers.speakers = CEA_SPEAKER_FRONT_LEFT_RIGHT;
> + if (audio_channels > 2)
> + speakers.speakers |= CEA_SPEAKER_FRONT_CENTER |
> + CEA_SPEAKER_LFE |
> + CEA_SPEAKER_REAR_LEFT_RIGHT;
Not really a problem for this test but we are adding 6 speakers for
anything with greater than 2 channels.
> + block = (struct edid_cea_data_block *)&cea->data[offset];
> + offset += edid_cea_data_block_set_speaker_alloc(block, &speakers);
> +
> + edid_ext_set_cea(ext, offset, 0,
> + EDID_CEA_BASIC_AUDIO | EDID_CEA_UNDERSCAN |
> + EDID_CEA_YCBCR444 | EDID_CEA_YCBCR422);
> + edid_update_checksum(edid);
> +
> + return edid;
> +}
> +
> +static void force_edid_and_connector(data_t *data, const struct edid *edid)
> +{
> + kmstest_force_edid(data->drm_fd, data->output->config.connector, edid);
> + igt_skip_on_f(!kmstest_force_connector(data->drm_fd,
> + data->output->config.connector,
> + FORCE_CONNECTOR_ON),
> + "Could not force HDMI connector on\n");
> +}
> +
> +static void cleanup_connector(data_t *data)
> +{
> + if (data->output->pending_crtc) {
> + igt_plane_t *primary;
> +
> + primary = igt_output_get_plane_type(data->output,
> + DRM_PLANE_TYPE_PRIMARY);
> + igt_plane_set_fb(primary, NULL);
> + igt_output_set_crtc(data->output, NULL);
> + igt_display_commit2(&data->display, COMMIT_ATOMIC);
> + }
> +
> + igt_remove_fb(data->drm_fd, &data->fb);
> +
> + kmstest_force_connector(data->drm_fd,
> + data->output->config.connector,
> + FORCE_CONNECTOR_UNSPECIFIED);
> + kmstest_force_edid(data->drm_fd,
> + data->output->config.connector, NULL);
> +}
> +
> +static int try_modeset(data_t *data, const drmModeModeInfo *mode)
> +{
> + igt_plane_t *primary;
> + int ret;
> +
> + igt_display_reset(&data->display);
> +
> + igt_output_set_crtc(data->output, data->crtc);
> + igt_output_override_mode(data->output, mode);
> +
> + primary = igt_output_get_plane_type(data->output,
> + DRM_PLANE_TYPE_PRIMARY);
> +
> + igt_create_pattern_fb(data->drm_fd,
> + mode->hdisplay, mode->vdisplay,
> + DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR,
> + &data->fb);
> + igt_plane_set_fb(primary, &data->fb);
> +
> + ret = igt_display_try_commit_atomic(&data->display,
> + DRM_MODE_ATOMIC_ALLOW_MODESET,
> + NULL);
> + if (ret) {
> + igt_plane_set_fb(primary, NULL);
> + igt_output_set_crtc(data->output, NULL);
> + igt_remove_fb(data->drm_fd, &data->fb);
> + }
> +
> + return ret;
> +}
> +
> +static bool audio_is_active(void)
> +{
> + if (!eld_is_supported())
> + return false;
> +
> + return eld_has_igt();
> +}
> +
> +static unsigned int get_eld_rates(void)
> +{
> + struct eld_entry eld;
> +
> + if (!eld_get_igt(&eld))
> + return 0;
> +
> + if (eld.sads_len == 0)
> + return 0;
> +
> + return eld.sads[0].rates;
This is heavily dependent on the semantics that currently we only add
one SAD in build_eld. Let's document this atleast.
> +}
> +
> +static void log_eld_rates(unsigned int declared, unsigned int eld_rates)
> +{
> + char decl_str[128], eld_str[128], pruned_str[128];
> + unsigned int pruned = declared & ~eld_rates;
> +
> + rates_to_str(declared, decl_str, sizeof(decl_str));
> + rates_to_str(eld_rates, eld_str, sizeof(eld_str));
> + rates_to_str(pruned, pruned_str, sizeof(pruned_str));
> +
> + igt_info(" SAD declared: %s\n", decl_str);
> + igt_info(" ELD reports: %s\n", eld_str);
> + if (pruned)
> + igt_info(" Pruned: %s\n", pruned_str);
> +}
> +
> +static void assert_per_rate(const drmModeModeInfo *mode, int channels,
> + int pkts_avail, unsigned int eld_rates)
> +{
> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
> + int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
> + channels);
> + bool in_eld = eld_rates & rate_table[i].flag;
> +
> + /* A rate that can't fit must not appear in ELD */
> + igt_assert_f(!(req > pkts_avail && in_eld),
> + "%s: req=%d > avail=%d but rate present in ELD\n",
> + rate_table[i].name, req, pkts_avail);
This check does not protect againts cases where the driver over prunes.
It asserts true all req <= pkts_avail cases, irrespective of in_eld.
> + }
> +}
> +
> +static void log_per_rate_analysis(const drmModeModeInfo *mode,
> + int bpc, int channels,
> + int pkts_avail, unsigned int eld_rates)
> +{
> + const char *layout = (channels <= 2) ? "L0" : "L1";
> +
> + igt_info(" %-6s %-3s pkts: avail=%d\n",
> + "Rate", layout, pkts_avail);
> +
> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
> + int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
> + channels);
> + const char *expect = (req <= pkts_avail && pkts_avail > 0) ?
> + "fit" : "NO";
> + const char *eld_has = (eld_rates & rate_table[i].flag) ?
> + "yes" : "no";
> +
> + igt_info(" %5s: req=%d fit=%s (ELD: %s)\n",
> + rate_table[i].name, req, expect, eld_has);
> + }
> +}
> +
> +/* Run the BPC × channels matrix for a given mode/hblank. */
> +static void test_audio_bw_matrix(data_t *data, const drmModeModeInfo *mode)
> +{
> + int hblank = hblank_of(mode);
> +
> + igt_info("=== Audio BW matrix: %s hblank=%d ===\n",
> + mode->name, hblank);
> +
> + for (int b = 0; b < ARRAY_SIZE(bpc_values); b++) {
> + int bpc = bpc_values[b];
> +
> + for (int c = 0; c < ARRAY_SIZE(channel_values); c++) {
> + int channels = channel_values[c];
> + const struct edid *edid;
> + int pkts, ret;
> + bool audio;
> + unsigned int eld_rates;
> +
> + edid = build_edid(bpc, channels);
> + force_edid_and_connector(data, edid);
> +
> + igt_output_set_prop_value(data->output,
> + IGT_CONNECTOR_MAX_BPC, bpc);
> +
> + pkts = avail_pkts_per_line(bpc, hblank);
> +
> + igt_info("\n %dbpc %dch hblank=%d avail_pkts=%d\n",
> + bpc, channels, hblank, pkts);
> +
> + ret = try_modeset(data, mode);
> +
> + if (ret) {
> + igt_info(" modeset: REJECTED\n");
> + cleanup_connector(data);
> + continue;
> + }
> +
> + /* Allow ELD to propagate */
> + usleep(200 * 1000);
> +
> + audio = audio_is_active();
> + eld_rates = audio ? get_eld_rates() : 0;
> +
> + igt_info(" modeset: OK\n");
> + igt_info(" audio: %s\n", audio ? "active" : "inactive");
> +
> + igt_assert_f(!(pkts == 0 && audio),
> + "Audio active with 0 available packets\n");
> + igt_assert_f(!(pkts > 0 && !audio),
> + "Audio inactive with %d available packets\n",
> + pkts);
> +
> + if (audio) {
> + log_eld_rates(ALL_SAMPLE_RATES, eld_rates);
> + assert_per_rate(mode, channels, pkts,
> + eld_rates);
> + }
> +
> + log_per_rate_analysis(mode, bpc, channels,
> + pkts, eld_rates);
> +
> + cleanup_connector(data);
> + }
> + }
> +
> + igt_info("\n=== End matrix ===\n");
> +}
> +
> +static void test_audio_bw_supported(data_t *data)
> +{
> + test_audio_bw_matrix(data, &mode_1080p_hblank160);
> +}
> +
> +static void test_audio_bw_pruned(data_t *data)
> +{
> + test_audio_bw_matrix(data, &mode_1080p_hblank80);
> +}
> +
> +static void test_suspend_audio_recovery(data_t *data,
> + enum igt_suspend_state state)
> +{
> + const struct edid *edid;
> + bool audio_before, audio_after;
> + unsigned int rates_before, rates_after;
> + char before_str[128], after_str[128];
> + int ret;
> +
> + edid = build_edid(12, 2);
> + force_edid_and_connector(data, edid);
> +
> + igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
> +
> + ret = try_modeset(data, &mode_1080p_hblank80);
> + igt_require(ret == 0);
> +
> + usleep(200 * 1000);
Is this empirical? I understand it is being already being used
kms_hdmi_inject but 200ms looks like a lot.
> +
> + audio_before = audio_is_active();
> + rates_before = audio_before ? get_eld_rates() : 0;
> + rates_to_str(rates_before, before_str, sizeof(before_str));
> + igt_info("Before suspend: audio=%d rates=%s\n",
> + audio_before, before_str);
> +
> + igt_system_suspend_autoresume(state, SUSPEND_TEST_NONE);
> +
> + usleep(200 * 1000);
> +
> + audio_after = audio_is_active();
> + rates_after = audio_after ? get_eld_rates() : 0;
> + rates_to_str(rates_after, after_str, sizeof(after_str));
> + igt_info("After suspend: audio=%d rates=%s\n",
> + audio_after, after_str);
> +
> + igt_assert_eq(audio_before, audio_after);
> + if (audio_before)
> + igt_assert_eq(rates_before, rates_after);
> +
> + cleanup_connector(data);
> +}
> +
> +static void test_runtime_suspend_audio(data_t *data)
> +{
> + const struct edid *edid;
> + bool audio_before, audio_after;
> + unsigned int rates_before, rates_after;
> + char before_str[128], after_str[128];
> + int ret;
> +
> + edid = build_edid(12, 2);
> + force_edid_and_connector(data, edid);
> +
> + igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
> +
> + ret = try_modeset(data, &mode_1080p_hblank80);
> + igt_require(ret == 0);
> +
> + usleep(200 * 1000);
> +
> + audio_before = audio_is_active();
> + rates_before = audio_before ? get_eld_rates() : 0;
> + rates_to_str(rates_before, before_str, sizeof(before_str));
> + igt_info("Before runtime suspend: audio=%d rates=%s\n",
> + audio_before, before_str);
> +
> + kmstest_set_connector_dpms(data->drm_fd,
> + data->output->config.connector,
> + DRM_MODE_DPMS_OFF);
> + usleep(500 * 1000);
> + kmstest_set_connector_dpms(data->drm_fd,
> + data->output->config.connector,
> + DRM_MODE_DPMS_ON);
> + usleep(500 * 1000);
> +
> + audio_after = audio_is_active();
> + rates_after = audio_after ? get_eld_rates() : 0;
> + rates_to_str(rates_after, after_str, sizeof(after_str));
> + igt_info("After runtime suspend: audio=%d rates=%s\n",
> + audio_after, after_str);
> +
> + igt_assert_eq(audio_before, audio_after);
> + if (audio_before)
> + igt_assert_eq(rates_before, rates_after);
> +
> + cleanup_connector(data);
> +}
test_suspend_audio_recovery and test_runtime_suspend_audio are almost
identical. Can we converge them?
> +
> +int igt_main()
> +{
> + data_t data = {};
> +
> + igt_fixture() {
> + data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
> + igt_require(is_intel_device(data.drm_fd));
> + kmstest_set_vt_graphics_mode();
> + igt_display_require(&data.display, data.drm_fd);
> +
> + data.output = find_hdmi_output(&data.display);
> + igt_require_f(data.output, "No HDMI connector found\n");
> +
> + data.crtc = igt_first_crtc(&data.display);
> + igt_require_f(data.crtc, "No usable CRTC found\n");
> + }
> +
> + igt_describe("Baseline: hblank=160, audio should be fully supported "
> + "for all BPC and channel configurations.");
> + igt_subtest("audio-bw-supported")
> + test_audio_bw_supported(&data);
> +
> + igt_describe("Constrained: hblank=80 (CVT RB2), audio may be pruned "
> + "or disabled depending on BPC.");
> + igt_subtest("audio-bw-pruned")
> + test_audio_bw_pruned(&data);
> +
> + igt_describe("Validate audio recovery after S3 suspend with "
> + "constrained hblank.");
> + igt_subtest("suspend-s3-audio-recovery")
> + test_suspend_audio_recovery(&data, SUSPEND_STATE_MEM);
The documentation for igt_suspend_state says "A memory sleep
(non-hibernation) target state, respecting the system's mem_sleep
default" if your intention is to deterministically go to S3 use
SUSPEND_STATE_S3 instead.
May I know why only these two power states were selected in particular?
> +
> + igt_describe("Validate audio recovery after S4 hibernate with "
> + "constrained hblank.");
> + igt_subtest("suspend-s4-audio-recovery")
> + test_suspend_audio_recovery(&data, SUSPEND_STATE_DISK);
> +
> + igt_describe("Validate audio recovery after runtime suspend with "
> + "constrained hblank.");
> + igt_subtest("runtime-suspend-audio-recovery")
> + test_runtime_suspend_audio(&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 a62f447df..facb7ab5d 100644
> --- a/tests/meson.build
> +++ b/tests/meson.build
> @@ -259,6 +259,7 @@ intel_kms_progs = [
> 'kms_fbc_dirty_rect',
> 'kms_fbcon_fbt',
> 'kms_fence_pin_leak',
> + 'kms_hdmi_audio_bw',
needs to be alphabetical order?
> 'kms_flip_scaled_crc',
> 'kms_flip_tiling',
> 'kms_frontbuffer_tracking',
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH i-g-t 1/2] tests/intel: Add kms_hdmi_audio_bw test
2026-08-31 8:55 ` Borah, Chaitanya Kumar
@ 2026-09-01 12:03 ` Borah, Chaitanya Kumar
0 siblings, 0 replies; 4+ messages in thread
From: Borah, Chaitanya Kumar @ 2026-09-01 12:03 UTC (permalink / raw)
To: Swati Sharma, igt-dev
On 8/31/2026 2:25 PM, Borah, Chaitanya Kumar wrote:
>
>
> On 8/19/2026 1:08 PM, Swati Sharma wrote:
>> Add a new IGT test to validate HDMI TMDS audio bandwidth constraints
>> under constrained horizontal blanking intervals.
>>
>> The test injects EDIDs declaring all 7 CEA sample rates (32kHz-192kHz)
>> and observes which rates the driver exposes via ELD (EDID-Like Data)
>> under varying BPC and channel configurations.
>>
>> Subtests:
>> - audio-bw-supported: Baseline with hblank=160 where audio bandwidth
>> is sufficient for all configurations.
>> - audio-bw-pruned: Constrained hblank=80 (CVT RB2) where the driver
>> must prune unsustainable sample rates or disable audio entirely.
>> - suspend-s3/s4-audio-recovery: Verify audio state and sample rates
>> are preserved across system suspend/resume.
>> - runtime-suspend-audio-recovery: Verify audio state is preserved
>> across DPMS off/on cycles.
>>
>> The test uses the HDMI TMDS bandwidth formula from the spec:
>> pkts_avail = FLOOR((CEIL(hblank * bpc/8) - overhead) / 32)
>> pkts_reqd = CEIL(R_AP * T_line)
>> where overhead=74 (HDCP 1.4 rekey always reserved by the driver).
>>
>> Assertions verify:
>> - Audio is active when bandwidth is available (pkts_avail > 0)
>> - Audio is inactive when bandwidth is exhausted (pkts_avail == 0)
>> - No sample rate requiring more packets than available appears in ELD
>>
>> Signed-off-by: Swati Sharma <swati2.sharma@intel.com>
>> Assisted-by: GitHub Copilot:Claude Opus 4.6
>> ---
>> tests/intel/kms_hdmi_audio_bw.c | 626 ++++++++++++++++++++++++++++++++
>> tests/meson.build | 1 +
>> 2 files changed, 627 insertions(+)
>> create mode 100644 tests/intel/kms_hdmi_audio_bw.c
>>
>> diff --git a/tests/intel/kms_hdmi_audio_bw.c b/tests/intel/
>> kms_hdmi_audio_bw.c
>> new file mode 100644
>> index 000000000..4b6f0d001
>> --- /dev/null
>> +++ b/tests/intel/kms_hdmi_audio_bw.c
>> @@ -0,0 +1,626 @@
>> +// SPDX-License-Identifier: MIT
>> +/*
>> + * Copyright © 2026 Intel Corporation
>> + */
>> +
>> +/**
>> + * TEST: kms hdmi audio bw
>> + * Category: Display
>> + * Description: Validate HDMI TMDS audio bandwidth constraints by
>> injecting
>> + * EDIDs with all sample rates declared and observing
>> which rates
>> + * the driver exposes (via ELD) under varying BPC /
>> channel /
>> + * hblank configurations.
>> + * Driver requirement: i915, xe
>> + * Mega feature: Display Audio
>> + */
>> +
>> +#include "config.h"
>> +
>> +#include <math.h>
>> +#include <string.h>
>> +
>> +#include "igt.h"
>> +#include "igt_edid.h"
>> +#include "igt_eld.h"
>> +#include "igt_aux.h"
>> +#include "xe/xe_query.h"
>
> What is this used for?
>
>> +
>> +/**
>> + * SUBTEST: audio-bw-supported
>> + * Description: Baseline test with hblank=160 where audio bandwidth is
>> + * sufficient for all BPC and channel combinations.
>> Verifies
>> + * that no sample rates are pruned.
>> + *
>> + * SUBTEST: audio-bw-pruned
>> + * Description: Constrained test with hblank=80 (CVT RB2) where audio
>> + * bandwidth is limited. Logs which sample rates are pruned
>> + * per BPC and channel combination.
>> + *
>> + * SUBTEST: suspend-%s-audio-recovery
>> + * Description: Validate audio state restoration after %arg[1] with
>> + * constrained hblank=80 and 12bpc.
>> + *
>> + * arg[1]:
>> + *
>> + * @s3: S3 (suspend to RAM)
>> + * @s4: S4 (hibernate)
>> + *
>> + * SUBTEST: runtime-suspend-audio-recovery
>> + * Description: Validate audio state restoration after runtime
>> suspend/resume
>> + * with constrained hblank=80 and 12bpc.
>> + */
>> +
>> +IGT_TEST_DESCRIPTION("Validate HDMI TMDS audio bandwidth constraints. "
>> + "EDIDs declare all sample rates (32k-192k); the test "
>> + "observes which rates survive in the ELD under "
>> + "constrained hblank timings.");
>> +
>> +typedef struct {
>> + int drm_fd;
>> + igt_display_t display;
>> + igt_output_t *output;
>> + igt_crtc_t *crtc;
>> + struct igt_fb fb;
>> +} data_t;
>> +
>> +/* All sample rates declared in the EDID SAD */
>> +#define ALL_SAMPLE_RATES (CEA_SAD_SAMPLING_RATE_32KHZ | \
>> + CEA_SAD_SAMPLING_RATE_44KHZ | \
>> + CEA_SAD_SAMPLING_RATE_48KHZ | \
>> + CEA_SAD_SAMPLING_RATE_88KHZ | \
>> + CEA_SAD_SAMPLING_RATE_96KHZ | \
>> + CEA_SAD_SAMPLING_RATE_176KHZ | \
>> + CEA_SAD_SAMPLING_RATE_192KHZ)
>> +
>> +struct rate_info {
>> + unsigned int flag;
>> + const char *name;
>> + int freq_hz;
>> +};
>> +
>> +static const struct rate_info rate_table[] = {
>> + { CEA_SAD_SAMPLING_RATE_32KHZ, "32k", 32000 },
>> + { CEA_SAD_SAMPLING_RATE_44KHZ, "44.1k", 44100 },
>> + { CEA_SAD_SAMPLING_RATE_48KHZ, "48k", 48000 },
>> + { CEA_SAD_SAMPLING_RATE_88KHZ, "88k", 88200 },
I missed this, lets keep the naming consistent "88.2k"
>> + { CEA_SAD_SAMPLING_RATE_96KHZ, "96k", 96000 },
>> + { CEA_SAD_SAMPLING_RATE_176KHZ, "176k", 176400 },
"176.4k"
Also see
https://lore.kernel.org/igt-dev/20260901113313.627816-1-chaitanya.kumar.borah@intel.com/T/#u
>> + { CEA_SAD_SAMPLING_RATE_192KHZ, "192k", 192000 },
>> +};
>> +
>> +#define ACR_RATE_MAX 1500
>> +#define TOLERANCE_AUDIOCLK_PPM 1000
>> +#define TOLERANCE_PIXELCLK 0.005
>> +#define HBLANK_OVERHEAD_STD 30
>> +#define HBLANK_OVERHEAD_HDCP14 74
>> +#define DI_PACKET_SIZE 32
>> +
>> +static void rates_to_str(unsigned int rates, char *buf, size_t len)
>> +{
>> + int pos = 0;
>> +
>> + buf[0] = '\0';
>> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
>> + if (!(rates & rate_table[i].flag))
>> + continue;
>> + if (pos > 0)
>> + pos += snprintf(buf + pos, len - pos, ",");
>> + pos += snprintf(buf + pos, len - pos, "%s", rate_table[i].name);
>> + }
>> + if (pos == 0)
>> + snprintf(buf, len, "none");
>> +}
>> +
>> +static const int bpc_values[] = { 8, 10, 12 };
>> +static const int channel_values[] = { 2, 8 };
>> +
>> +/*
>> + * 1920x1080@60Hz CVT RB2 — hblank=80 (constrained)
>> + * Available Packets/Line = FLOOR(((BPC/8)*80 - 74) / 32)
>> + * 8bpc=0, 10bpc=0, 12bpc=1
>> + */
>> +static const drmModeModeInfo mode_1080p_hblank80 = {
>> + .clock = 133320,
>> + .hdisplay = 1920,
>> + .hsync_start = 1928,
>> + .hsync_end = 1960,
>> + .htotal = 2000, /* hblank = 80 */
>> + .vdisplay = 1080,
>> + .vsync_start = 1097,
>> + .vsync_end = 1105,
>> + .vtotal = 1111,
>> + .vrefresh = 60,
>> + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
>> + .type = DRM_MODE_TYPE_DRIVER,
>> + .name = "1920x1080",
>> +};
>> +
>> +/*
>> + * 1920x1080@60Hz with hblank=160 (relaxed baseline)
>> + * Enough hblank for audio at any BPC.
>> + */
>> +static const drmModeModeInfo mode_1080p_hblank160 = {
>> + .clock = 148500,
>> + .hdisplay = 1920,
>> + .hsync_start = 1968,
>> + .hsync_end = 2000,
>> + .htotal = 2080, /* hblank = 160 */
>> + .vdisplay = 1080,
>> + .vsync_start = 1097,
>> + .vsync_end = 1105,
>> + .vtotal = 1111,
>> + .vrefresh = 60,
>> + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
>> + .type = DRM_MODE_TYPE_DRIVER,
>> + .name = "1920x1080",
>> +};
>> +
>> +static igt_output_t *find_hdmi_output(igt_display_t *display)
>> +{
>> + igt_output_t *output;
>> +
>> + for_each_connected_output(display, output) {
>> + drmModeConnector *c = output->config.connector;
>> +
>> + if (c->connector_type == DRM_MODE_CONNECTOR_HDMIA ||
>> + c->connector_type == DRM_MODE_CONNECTOR_HDMIB)
>> + return output;
>> + }
>> +
>> + return NULL;
>> +}
>> +
>> +static int hblank_of(const drmModeModeInfo *mode)
>> +{
>> + return mode->htotal - mode->hdisplay;
>> +}
>> +
>> +/*
>> + * Driver always reserves HDCP 1.4 rekey overhead (74 clocks) even when
>> + * HDCP is not active: 30 (standard) + 44 (HDCP 1.4 rekey quiet period).
>> + * FIXME: once driver exposes HDCP state, use 30 for no HDCP, 74 for
>> HDCP 1.4.
>> + */
>> +static int avail_pkts_per_line(int bpc, int hblank)
>> +{
>> + int overhead = HBLANK_OVERHEAD_HDCP14;
>> + int tb_blank = (bpc * hblank + 7) / 8; /* CEIL(hblank * bpc/8) */
>> + int avail = (tb_blank - overhead) / DI_PACKET_SIZE;
>> +
>> + return avail > 0 ? avail : 0;
>> +}
>> +
>> +/* Packets required per line for a given audio rate and channel
>> layout */
>> +static int required_pkts_per_line(const drmModeModeInfo *mode, int
>> freq_hz,
>> + int channels)
>> +{
>> + double ap = (channels <= 2) ? 0.25 : 1.0;
>> + double f_pixel_max = mode->clock * 1000.0 * (1 +
>> TOLERANCE_PIXELCLK);
>> + double t_line = mode->htotal / f_pixel_max;
>> + double r_ap = ((freq_hz * ap) + (2 * ACR_RATE_MAX)) *
>> + (1 + TOLERANCE_AUDIOCLK_PPM / 1e6);
>> +
>> + double avg_pkts = r_ap * t_line;
>> +
>> + return (int)avg_pkts + (avg_pkts > (int)avg_pkts ? 1 : 0);
>> +}
>> +
>> +/*
>> + * Build a CEA EDID declaring all 7 sample rates in the SAD.
>> + * Deep-color flags in HDMI VSDB match the requested bpc.
>> + */
>> +static const struct edid *
>> +build_edid(int bpc, int audio_channels)
>> +{
>> + static unsigned char raw_edid[2 * EDID_BLOCK_SIZE];
>> + struct edid *edid;
>> + struct edid_ext *ext;
>> + struct edid_cea *cea;
>> + struct edid_cea_data_block *block;
>> + struct cea_sad sad;
>> + struct hdmi_vsdb hdmi;
>
> nit: Let's call it vsdb.
>
>> + struct cea_speaker_alloc speakers;
>> + size_t offset = 0;
>> +
>> + memset(raw_edid, 0, sizeof(raw_edid));
>> + edid = (struct edid *)raw_edid;
>> + memcpy(edid, igt_kms_get_base_edid(), sizeof(struct edid));
>> + edid->extensions_len = 1;
>> +
>> + ext = &edid->extensions[0];
>> + cea = &ext->data.cea;
>> +
>> + if (audio_channels > 0) {
>> + cea_sad_init_pcm(&sad,
>> + audio_channels,
>> + ALL_SAMPLE_RATES,
>> + CEA_SAD_SAMPLE_SIZE_16 |
>> + CEA_SAD_SAMPLE_SIZE_24);
>> + block = (struct edid_cea_data_block *)&cea->data[offset];
>> + offset += edid_cea_data_block_set_sad(block, &sad, 1);
>> + }
>> +
>> + memset(&hdmi, 0, sizeof(hdmi));
>> + hdmi.src_phy_addr[0] = 0x10;
>> + hdmi.src_phy_addr[1] = 0x00;
>> + hdmi.flags1 = HDMI_VSDB_SUPPORTS_AI;
>> + hdmi.max_tdms_clock = 340000000 / (5 * 1000000);
>
> where do these number come from?
>
>> +
>> + switch (bpc) {
>> + case 12:
>> + hdmi.flags1 |= HDMI_VSDB_DC_36BIT;
>> + /* fall through */
>> + case 10:
>> + hdmi.flags1 |= HDMI_VSDB_DC_30BIT;
>> + /* fall through */
>> + case 8:
>> + break;
>> + }
>> +
>> + block = (struct edid_cea_data_block *)&cea->data[offset];
>> + offset += edid_cea_data_block_set_hdmi_vsdb(block, &hdmi,
>> + sizeof(hdmi));
>> +
>> + memset(&speakers, 0, sizeof(speakers));
>> + speakers.speakers = CEA_SPEAKER_FRONT_LEFT_RIGHT;
>> + if (audio_channels > 2)
>> + speakers.speakers |= CEA_SPEAKER_FRONT_CENTER |
>> + CEA_SPEAKER_LFE |
>> + CEA_SPEAKER_REAR_LEFT_RIGHT;
>
> Not really a problem for this test but we are adding 6 speakers for
> anything with greater than 2 channels.
>
>
>> + block = (struct edid_cea_data_block *)&cea->data[offset];
>> + offset += edid_cea_data_block_set_speaker_alloc(block, &speakers);
>> +
>> + edid_ext_set_cea(ext, offset, 0,
>> + EDID_CEA_BASIC_AUDIO | EDID_CEA_UNDERSCAN |
>> + EDID_CEA_YCBCR444 | EDID_CEA_YCBCR422);
>> + edid_update_checksum(edid);
>> +
>> + return edid;
>> +}
>> +
>> +static void force_edid_and_connector(data_t *data, const struct edid
>> *edid)
>> +{
>> + kmstest_force_edid(data->drm_fd, data->output->config.connector,
>> edid);
>> + igt_skip_on_f(!kmstest_force_connector(data->drm_fd,
>> + data->output->config.connector,
>> + FORCE_CONNECTOR_ON),
>> + "Could not force HDMI connector on\n");
>> +}
>> +
>> +static void cleanup_connector(data_t *data)
>> +{
>> + if (data->output->pending_crtc) {
>> + igt_plane_t *primary;
>> +
>> + primary = igt_output_get_plane_type(data->output,
>> + DRM_PLANE_TYPE_PRIMARY);
>> + igt_plane_set_fb(primary, NULL);
>> + igt_output_set_crtc(data->output, NULL);
>> + igt_display_commit2(&data->display, COMMIT_ATOMIC);
>> + }
>> +
>> + igt_remove_fb(data->drm_fd, &data->fb);
>> +
>> + kmstest_force_connector(data->drm_fd,
>> + data->output->config.connector,
>> + FORCE_CONNECTOR_UNSPECIFIED);
>> + kmstest_force_edid(data->drm_fd,
>> + data->output->config.connector, NULL);
>> +}
>> +
>> +static int try_modeset(data_t *data, const drmModeModeInfo *mode)
>> +{
>> + igt_plane_t *primary;
>> + int ret;
>> +
>> + igt_display_reset(&data->display);
>> +
>> + igt_output_set_crtc(data->output, data->crtc);
>> + igt_output_override_mode(data->output, mode);
>> +
>> + primary = igt_output_get_plane_type(data->output,
>> + DRM_PLANE_TYPE_PRIMARY);
>> +
>> + igt_create_pattern_fb(data->drm_fd,
>> + mode->hdisplay, mode->vdisplay,
>> + DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR,
>> + &data->fb);
>> + igt_plane_set_fb(primary, &data->fb);
>> +
>> + ret = igt_display_try_commit_atomic(&data->display,
>> + DRM_MODE_ATOMIC_ALLOW_MODESET,
>> + NULL);
>> + if (ret) {
>> + igt_plane_set_fb(primary, NULL);
>> + igt_output_set_crtc(data->output, NULL);
>> + igt_remove_fb(data->drm_fd, &data->fb);
>> + }
>> +
>> + return ret;
>> +}
>> +
>> +static bool audio_is_active(void)
>> +{
>> + if (!eld_is_supported())
>> + return false;
>> +
>> + return eld_has_igt();
>> +}
>> +
>> +static unsigned int get_eld_rates(void)
>> +{
>> + struct eld_entry eld;
>> +
>> + if (!eld_get_igt(&eld))
>> + return 0;
>> +
>> + if (eld.sads_len == 0)
>> + return 0;
>> +
>> + return eld.sads[0].rates;
>
> This is heavily dependent on the semantics that currently we only add
> one SAD in build_eld. Let's document this atleast.
>
>> +}
>> +
>> +static void log_eld_rates(unsigned int declared, unsigned int eld_rates)
>> +{
>> + char decl_str[128], eld_str[128], pruned_str[128];
>> + unsigned int pruned = declared & ~eld_rates;
>> +
>> + rates_to_str(declared, decl_str, sizeof(decl_str));
>> + rates_to_str(eld_rates, eld_str, sizeof(eld_str));
>> + rates_to_str(pruned, pruned_str, sizeof(pruned_str));
>> +
>> + igt_info(" SAD declared: %s\n", decl_str);
>> + igt_info(" ELD reports: %s\n", eld_str);
>> + if (pruned)
>> + igt_info(" Pruned: %s\n", pruned_str);
>> +}
>> +
>> +static void assert_per_rate(const drmModeModeInfo *mode, int channels,
>> + int pkts_avail, unsigned int eld_rates)
>> +{
>> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
>> + int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
>> + channels);
>> + bool in_eld = eld_rates & rate_table[i].flag;
>> +
>> + /* A rate that can't fit must not appear in ELD */
>> + igt_assert_f(!(req > pkts_avail && in_eld),
>> + "%s: req=%d > avail=%d but rate present in ELD\n",
>> + rate_table[i].name, req, pkts_avail);
>
> This check does not protect againts cases where the driver over prunes.
> It asserts true all req <= pkts_avail cases, irrespective of in_eld.
>
>> + }
>> +}
>> +
>> +static void log_per_rate_analysis(const drmModeModeInfo *mode,
>> + int bpc, int channels,
>> + int pkts_avail, unsigned int eld_rates)
>> +{
>> + const char *layout = (channels <= 2) ? "L0" : "L1";
>> +
>> + igt_info(" %-6s %-3s pkts: avail=%d\n",
>> + "Rate", layout, pkts_avail);
>> +
>> + for (int i = 0; i < ARRAY_SIZE(rate_table); i++) {
>> + int req = required_pkts_per_line(mode, rate_table[i].freq_hz,
>> + channels);
>> + const char *expect = (req <= pkts_avail && pkts_avail > 0) ?
>> + "fit" : "NO";
>> + const char *eld_has = (eld_rates & rate_table[i].flag) ?
>> + "yes" : "no";
>> +
>> + igt_info(" %5s: req=%d fit=%s (ELD: %s)\n",
>> + rate_table[i].name, req, expect, eld_has);
>> + }
>> +}
>> +
>> +/* Run the BPC × channels matrix for a given mode/hblank. */
>> +static void test_audio_bw_matrix(data_t *data, const drmModeModeInfo
>> *mode)
>> +{
>> + int hblank = hblank_of(mode);
>> +
>> + igt_info("=== Audio BW matrix: %s hblank=%d ===\n",
>> + mode->name, hblank);
>> +
>> + for (int b = 0; b < ARRAY_SIZE(bpc_values); b++) {
>> + int bpc = bpc_values[b];
>> +
>> + for (int c = 0; c < ARRAY_SIZE(channel_values); c++) {
>> + int channels = channel_values[c];
>> + const struct edid *edid;
>> + int pkts, ret;
>> + bool audio;
>> + unsigned int eld_rates;
>> +
>> + edid = build_edid(bpc, channels);
>> + force_edid_and_connector(data, edid);
>> +
>> + igt_output_set_prop_value(data->output,
>> + IGT_CONNECTOR_MAX_BPC, bpc);
>> +
>> + pkts = avail_pkts_per_line(bpc, hblank);
>> +
>> + igt_info("\n %dbpc %dch hblank=%d avail_pkts=%d\n",
>> + bpc, channels, hblank, pkts);
>> +
>> + ret = try_modeset(data, mode);
>> +
>> + if (ret) {
>> + igt_info(" modeset: REJECTED\n");
>> + cleanup_connector(data);
>> + continue;
>> + }
>> +
>> + /* Allow ELD to propagate */
>> + usleep(200 * 1000);
>> +
>> + audio = audio_is_active();
>> + eld_rates = audio ? get_eld_rates() : 0;
>> +
>> + igt_info(" modeset: OK\n");
>> + igt_info(" audio: %s\n", audio ? "active" :
>> "inactive");
>> +
>> + igt_assert_f(!(pkts == 0 && audio),
>> + "Audio active with 0 available packets\n");
>> + igt_assert_f(!(pkts > 0 && !audio),
>> + "Audio inactive with %d available packets\n",
>> + pkts);
>> +
>> + if (audio) {
>> + log_eld_rates(ALL_SAMPLE_RATES, eld_rates);
>> + assert_per_rate(mode, channels, pkts,
>> + eld_rates);
>> + }
>> +
>> + log_per_rate_analysis(mode, bpc, channels,
>> + pkts, eld_rates);
>> +
>> + cleanup_connector(data);
>> + }
>> + }
>> +
>> + igt_info("\n=== End matrix ===\n");
>> +}
>> +
>> +static void test_audio_bw_supported(data_t *data)
>> +{
>> + test_audio_bw_matrix(data, &mode_1080p_hblank160);
>> +}
>> +
>> +static void test_audio_bw_pruned(data_t *data)
>> +{
>> + test_audio_bw_matrix(data, &mode_1080p_hblank80);
>> +}
>> +
>> +static void test_suspend_audio_recovery(data_t *data,
>> + enum igt_suspend_state state)
>> +{
>> + const struct edid *edid;
>> + bool audio_before, audio_after;
>> + unsigned int rates_before, rates_after;
>> + char before_str[128], after_str[128];
>> + int ret;
>> +
>> + edid = build_edid(12, 2);
>> + force_edid_and_connector(data, edid);
>> +
>> + igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
>> +
>> + ret = try_modeset(data, &mode_1080p_hblank80);
>> + igt_require(ret == 0);
>> +
>> + usleep(200 * 1000);
>
> Is this empirical? I understand it is being already being used
> kms_hdmi_inject but 200ms looks like a lot.
>
>> +
>> + audio_before = audio_is_active();
>> + rates_before = audio_before ? get_eld_rates() : 0;
>> + rates_to_str(rates_before, before_str, sizeof(before_str));
>> + igt_info("Before suspend: audio=%d rates=%s\n",
>> + audio_before, before_str);
>> +
>> + igt_system_suspend_autoresume(state, SUSPEND_TEST_NONE);
>> +
>> + usleep(200 * 1000);
>> +
>> + audio_after = audio_is_active();
>> + rates_after = audio_after ? get_eld_rates() : 0;
>> + rates_to_str(rates_after, after_str, sizeof(after_str));
>> + igt_info("After suspend: audio=%d rates=%s\n",
>> + audio_after, after_str);
>> +
>> + igt_assert_eq(audio_before, audio_after);
>> + if (audio_before)
>> + igt_assert_eq(rates_before, rates_after);
>> +
>> + cleanup_connector(data);
>> +}
>> +
>> +static void test_runtime_suspend_audio(data_t *data)
>> +{
>> + const struct edid *edid;
>> + bool audio_before, audio_after;
>> + unsigned int rates_before, rates_after;
>> + char before_str[128], after_str[128];
>> + int ret;
>> +
>> + edid = build_edid(12, 2);
>> + force_edid_and_connector(data, edid);
>> +
>> + igt_output_set_prop_value(data->output, IGT_CONNECTOR_MAX_BPC, 12);
>> +
>> + ret = try_modeset(data, &mode_1080p_hblank80);
>> + igt_require(ret == 0);
>> +
>> + usleep(200 * 1000);
>> +
>> + audio_before = audio_is_active();
>> + rates_before = audio_before ? get_eld_rates() : 0;
>> + rates_to_str(rates_before, before_str, sizeof(before_str));
>> + igt_info("Before runtime suspend: audio=%d rates=%s\n",
>> + audio_before, before_str);
>> +
>> + kmstest_set_connector_dpms(data->drm_fd,
>> + data->output->config.connector,
>> + DRM_MODE_DPMS_OFF);
>> + usleep(500 * 1000);
>> + kmstest_set_connector_dpms(data->drm_fd,
>> + data->output->config.connector,
>> + DRM_MODE_DPMS_ON);
>> + usleep(500 * 1000);
>> +
>> + audio_after = audio_is_active();
>> + rates_after = audio_after ? get_eld_rates() : 0;
>> + rates_to_str(rates_after, after_str, sizeof(after_str));
>> + igt_info("After runtime suspend: audio=%d rates=%s\n",
>> + audio_after, after_str);
>> +
>> + igt_assert_eq(audio_before, audio_after);
>> + if (audio_before)
>> + igt_assert_eq(rates_before, rates_after);
>> +
>> + cleanup_connector(data);
>> +}
>
> test_suspend_audio_recovery and test_runtime_suspend_audio are almost
> identical. Can we converge them?
>
>> +
>> +int igt_main()
>> +{
>> + data_t data = {};
>> +
>> + igt_fixture() {
>> + data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
>> + igt_require(is_intel_device(data.drm_fd));
>> + kmstest_set_vt_graphics_mode();
>> + igt_display_require(&data.display, data.drm_fd);
>> +
>> + data.output = find_hdmi_output(&data.display);
>> + igt_require_f(data.output, "No HDMI connector found\n");
>> +
>> + data.crtc = igt_first_crtc(&data.display);
>> + igt_require_f(data.crtc, "No usable CRTC found\n");
>> + }
>> +
>> + igt_describe("Baseline: hblank=160, audio should be fully
>> supported "
>> + "for all BPC and channel configurations.");
>> + igt_subtest("audio-bw-supported")
>> + test_audio_bw_supported(&data);
>> +
>> + igt_describe("Constrained: hblank=80 (CVT RB2), audio may be
>> pruned "
>> + "or disabled depending on BPC.");
>> + igt_subtest("audio-bw-pruned")
>> + test_audio_bw_pruned(&data);
We have to re-think the sub test names.
audio-bw-supported seems to prune some SADs.
Opened device: /dev/dri/card0
Starting subtest: audio-bw-supported
=== Audio BW matrix: 1920x1080 hblank=160 ===
...
8bpc 8ch hblank=160 avail_pkts=2
modeset: OK
audio: active
SAD declared: 32k,44.1k,48k,88k,96k,176k,192k
ELD reports: 32k,44.1k,48k,88k,96k
Pruned: 176k,192k
Rate L1 pkts: avail=2
32k: req=1 fit=fit (ELD: yes)
44.1k: req=1 fit=fit (ELD: yes)
48k: req=1 fit=fit (ELD: yes)
88k: req=2 fit=fit (ELD: yes)
96k: req=2 fit=fit (ELD: yes)
176k: req=3 fit=NO (ELD: no)
192k: req=3 fit=NO (ELD: no)
While audio-bw-pruned has a configuration that prunes nothing.
Starting subtest: audio-bw-pruned
=== Audio BW matrix: 1920x1080 hblank=80 ===
...
12bpc 2ch hblank=80 avail_pkts=1
modeset: OK
audio: active
SAD declared: 32k,44.1k,48k,88k,96k,176k,192k
ELD reports: 32k,44.1k,48k,88k,96k,176k,192k
Rate L0 pkts: avail=1
32k: req=1 fit=fit (ELD: yes)
44.1k: req=1 fit=fit (ELD: yes)
48k: req=1 fit=fit (ELD: yes)
88k: req=1 fit=fit (ELD: yes)
96k: req=1 fit=fit (ELD: yes)
176k: req=1 fit=fit (ELD: yes)
192k: req=1 fit=fit (ELD: yes)
Either fold both of them to the same subtest or you can have names like
audio-bw-check-rb1 and audio-bw-check-rb2
==
Chaitanya
>> +
>> + igt_describe("Validate audio recovery after S3 suspend with "
>> + "constrained hblank.");
>> + igt_subtest("suspend-s3-audio-recovery")
>> + test_suspend_audio_recovery(&data, SUSPEND_STATE_MEM);
>
> The documentation for igt_suspend_state says "A memory sleep (non-
> hibernation) target state, respecting the system's mem_sleep default" if
> your intention is to deterministically go to S3 use SUSPEND_STATE_S3
> instead.
>
> May I know why only these two power states were selected in particular?
>> +
>> + igt_describe("Validate audio recovery after S4 hibernate with "
>> + "constrained hblank.");
>> + igt_subtest("suspend-s4-audio-recovery")
>> + test_suspend_audio_recovery(&data, SUSPEND_STATE_DISK);
>> +
>> + igt_describe("Validate audio recovery after runtime suspend with "
>> + "constrained hblank.");
>> + igt_subtest("runtime-suspend-audio-recovery")
>> + test_runtime_suspend_audio(&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 a62f447df..facb7ab5d 100644
>> --- a/tests/meson.build
>> +++ b/tests/meson.build
>> @@ -259,6 +259,7 @@ intel_kms_progs = [
>> 'kms_fbc_dirty_rect',
>> 'kms_fbcon_fbt',
>> 'kms_fence_pin_leak',
>> + 'kms_hdmi_audio_bw',
>
> needs to be alphabetical order?
>
>> 'kms_flip_scaled_crc',
>> 'kms_flip_tiling',
>> 'kms_frontbuffer_tracking',
>
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-01 12:04 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-14 12:16 [PATCH i-g-t 1/2] tests/intel: Add kms_hdmi_audio_bw test Swati Sharma
-- strict thread matches above, loose matches on Subject: below --
2026-08-19 7:38 Swati Sharma
2026-08-31 8:55 ` Borah, Chaitanya Kumar
2026-09-01 12:03 ` Borah, Chaitanya Kumar
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox