From 23672152b996629248909ece2ead77eeb9e252b1 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Sat, 25 Jul 2026 20:55:15 +0100 Subject: [PATCH 1/4] drm/edid: Accept 3D_Detail_X of 0000 for side-by-side (half) modes When parsing the per-VIC 3D_Structure_X entries of the HDMI VSDB, a side-by-side (half) entry is only accepted if the accompanying 3D_Detail_X field is 0001, "horizontal sub-sampling". HDMI 1.4b Table H-8 also defines 0000, "all of the horizontal sub-sampling and Quincunx matrix methods", which is a superset of 0001. The remaining defined values, 0110 to 1001, each select one specific Quincunx matrix variant, and everything else is reserved. DRM never populates hdmi_vendor_infoframe.s3d_ext_data, so the 3D_Ext_Data it transmits for a side-by-side (half) mode is 0000, "horizontal sub-sampling, odd left and odd right picture". A sink advertising 3D_Detail_X of 0000 supports that, so the mode should be offered. Instead the entry is silently discarded and such a sink ends up with no side-by-side modes at all. Seen on a Panasonic TX-P50VT30B, which reports 3D_Detail_X = 0000 for all of its side-by-side entries: 3D VIC indices with specific capabilities: VIC 16: 1920x1080 60.000000 Hz 16:9 (side-by-side, any subsampling) ... Fixes: 0e5083aa9d47 ("drm/edid: parse the list of additional 3D modes") Signed-off-by: Dom Cobley --- drivers/gpu/drm/drm_edid.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/drm_edid.c b/drivers/gpu/drm/drm_edid.c index 28069e5863679f..8f777ca837271e 100644 --- a/drivers/gpu/drm/drm_edid.c +++ b/drivers/gpu/drm/drm_edid.c @@ -4920,8 +4920,8 @@ do_hdmi_vsdb_modes(struct drm_connector *connector, const u8 *db, u8 len) newflag = DRM_MODE_FLAG_3D_TOP_AND_BOTTOM; break; case 8: - /* 3D_Detail_X */ - if ((db[9 + offset + i] >> 4) == 1) + /* 3D_Detail_X: 0 and 1 both include horizontal sub-sampling */ + if ((db[9 + offset + i] >> 4) <= 1) newflag = DRM_MODE_FLAG_3D_SIDE_BY_SIDE_HALF; break; } From d504baf1bd2071d5cf30178532526903996e7887 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Mon, 27 Jul 2026 14:31:17 +0100 Subject: [PATCH 2/4] drm/display/hdmi: Account for frame packing in the TMDS character rate An HDMI frame packing 3D mode transmits the left and right eye views, separated by an active space gap, within a single frame period. Per HDMI 1.4b section 8.2.3.2 the resulting raster has twice the vertical total of the base 2D mode, so the pixel clock - and with it the TMDS character rate - is doubled. drm_hdmi_compute_mode_clock() derives the rate from mode->clock, which for a stereo mode still describes the base 2D timing; the doubling only appears in the crtc_* timings, and only once a driver asks for it with CRTC_STEREO_DOUBLE. The computed rate was therefore half the real one, so the sink's max_tmds_clock was checked against the wrong value and drivers programmed half the pixel clock the mode needs. Only a driver that both advertises stereo modes and uses the HDMI connector helpers can see a change here, and vc4 is currently the only one. i915 and nouveau set connector->stereo_allowed but compute their TMDS rates themselves. The remaining callers of drm_hdmi_compute_mode_clock() - ite-it6263, dw_hdmi_qp-rockchip, inno_hdmi and sun4i_hdmi - leave stereo_allowed clear, so drm_mode_validate_flag() prunes 3D modes from their connectors and none ever reaches this function. Should userspace hand such a driver a 3D-flagged mode directly, it is now rejected as exceeding the sink's TMDS limit rather than being driven at half rate. Non-stereo modes, and the 3D layouts that fit within the 2D timing (side-by-side, top-and-bottom, and friends), are unaffected. Signed-off-by: Dom Cobley --- drivers/gpu/drm/display/drm_hdmi_helper.c | 8 ++++++++ drivers/gpu/drm/tests/drm_connector_test.c | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/drivers/gpu/drm/display/drm_hdmi_helper.c b/drivers/gpu/drm/display/drm_hdmi_helper.c index a237dc55805d0d..1534b1a4795ba9 100644 --- a/drivers/gpu/drm/display/drm_hdmi_helper.c +++ b/drivers/gpu/drm/display/drm_hdmi_helper.c @@ -254,6 +254,14 @@ drm_hdmi_compute_mode_clock(const struct drm_display_mode *mode, if (mode->flags & DRM_MODE_FLAG_DBLCLK) clock = clock * 2; + /* + * HDMI 1.4b Spec, Section 8.2.3.2 - Frame Packing Structure + * transmits both eyes within a single frame, at twice the vertical + * total, and thus twice the pixel clock, of the base 2D mode. + */ + if ((mode->flags & DRM_MODE_FLAG_3D_MASK) == DRM_MODE_FLAG_3D_FRAME_PACKING) + clock = clock * 2; + return DIV_ROUND_CLOSEST_ULL(clock * bpc, 8); } EXPORT_SYMBOL(drm_hdmi_compute_mode_clock); diff --git a/drivers/gpu/drm/tests/drm_connector_test.c b/drivers/gpu/drm/tests/drm_connector_test.c index 22e2d959eb3145..6cfdce96feb8bf 100644 --- a/drivers/gpu/drm/tests/drm_connector_test.c +++ b/drivers/gpu/drm/tests/drm_connector_test.c @@ -1625,6 +1625,27 @@ static void drm_test_drm_hdmi_compute_mode_clock_rgb_double(struct kunit *test) KUNIT_EXPECT_EQ(test, (mode->clock * 1000ULL) * 2, rate); } +/* + * Test that for a frame packing stereo mode, the TMDS character rate is + * indeed double the mode pixel clock. + */ +static void drm_test_drm_hdmi_compute_mode_clock_stereo_frame_packing(struct kunit *test) +{ + struct drm_connector_init_priv *priv = test->priv; + struct drm_display_mode *mode; + unsigned long long rate; + struct drm_device *drm = &priv->drm; + + mode = drm_kunit_display_mode_from_cea_vic(test, drm, 32); + KUNIT_ASSERT_NOT_NULL(test, mode); + + mode->flags |= DRM_MODE_FLAG_3D_FRAME_PACKING; + + rate = drm_hdmi_compute_mode_clock(mode, 8, HDMI_COLORSPACE_RGB); + KUNIT_ASSERT_GT(test, rate, 0); + KUNIT_EXPECT_EQ(test, (mode->clock * 1000ULL) * 2, rate); +} + /* * Test that the TMDS character rate computation for the VIC modes * explicitly listed in the spec as supporting YUV420 succeed and return @@ -1784,6 +1805,7 @@ static struct kunit_case drm_hdmi_compute_mode_clock_tests[] = { KUNIT_CASE(drm_test_drm_hdmi_compute_mode_clock_rgb_12bpc), KUNIT_CASE(drm_test_drm_hdmi_compute_mode_clock_rgb_12bpc_vic_1), KUNIT_CASE(drm_test_drm_hdmi_compute_mode_clock_rgb_double), + KUNIT_CASE(drm_test_drm_hdmi_compute_mode_clock_stereo_frame_packing), KUNIT_CASE_PARAM(drm_test_connector_hdmi_compute_mode_clock_yuv420_valid, drm_hdmi_compute_mode_clock_yuv420_valid_gen_params), KUNIT_CASE(drm_test_connector_hdmi_compute_mode_clock_yuv420_10_bpc), From c141e4d3b9e44531a90cf57ab6898e8cc1a78534 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Mon, 27 Jul 2026 14:31:35 +0100 Subject: [PATCH 3/4] drm/vc4: Derive scanout rate and frame size from the CRTC timings The vertical timings written to the pixelvalve already come from the crtc_* fields of the adjusted mode, but three places that describe the same raster still read the mode->* fields directly: - the HVS bandwidth estimate in vc4_crtc_atomic_check() - the audio CTS divisor in vc4_hdmi_set_n_cts() - the HVS display FIFO width and height in {vc4,vc6}_hvs_init_channel() The two sets of fields differ only for stereo modes that pack more than one eye into a frame, which vc4 does not yet handle, so switch them over to mode->crtc_clock and drm_mode_get_hv_timing() ahead of adding that support. drm_mode_get_hv_timing() is used rather than crtc_vdisplay because the HVS is fed a whole frame even for interlaced modes, whose crtc_vdisplay describes a single field. No functional change. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_crtc.c | 7 ++++--- drivers/gpu/drm/vc4/vc4_hdmi.c | 2 +- drivers/gpu/drm/vc4/vc4_hvs.c | 20 ++++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_crtc.c b/drivers/gpu/drm/vc4/vc4_crtc.c index 64265651dbb2e0..9de390fe3794cd 100644 --- a/drivers/gpu/drm/vc4/vc4_crtc.c +++ b/drivers/gpu/drm/vc4/vc4_crtc.c @@ -768,10 +768,11 @@ int vc4_crtc_atomic_check(struct drm_crtc *crtc, struct vc4_encoder *vc4_encoder = to_vc4_encoder(encoder); if (vc4_encoder->type == VC4_ENCODER_TYPE_HDMI0) { - vc4_state->hvs_load = max(mode->clock * mode->hdisplay / mode->htotal + 8000, - mode->clock * 9 / 10) * 1000; + vc4_state->hvs_load = + max(mode->crtc_clock * mode->hdisplay / mode->htotal + 8000, + mode->crtc_clock * 9 / 10) * 1000; } else { - vc4_state->hvs_load = mode->clock * 1000; + vc4_state->hvs_load = mode->crtc_clock * 1000; } } diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c index c111b61175f044..78c52f8a0cb9a3 100644 --- a/drivers/gpu/drm/vc4/vc4_hdmi.c +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c @@ -1853,7 +1853,7 @@ static void vc4_hdmi_set_n_cts(struct vc4_hdmi *vc4_hdmi, unsigned int samplerat lockdep_assert_held(&vc4_hdmi->hw_lock); n = 128 * samplerate / 1000; - tmp = (u64)(mode->clock * 1000) * n; + tmp = (u64)(mode->crtc_clock * 1000) * n; do_div(tmp, 128 * samplerate); cts = tmp; diff --git a/drivers/gpu/drm/vc4/vc4_hvs.c b/drivers/gpu/drm/vc4/vc4_hvs.c index 4ae905855bb466..aa51aba723a38d 100644 --- a/drivers/gpu/drm/vc4/vc4_hvs.c +++ b/drivers/gpu/drm/vc4/vc4_hvs.c @@ -945,6 +945,7 @@ static int vc4_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, struct vc4_crtc_state *vc4_crtc_state = to_vc4_crtc_state(crtc->state); unsigned int chan = vc4_crtc_state->assigned_channel; bool interlace = mode->flags & DRM_MODE_FLAG_INTERLACE; + int hdisplay, vdisplay; u32 dispbkgndx; u32 dispctrl; int idx; @@ -954,6 +955,9 @@ static int vc4_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, if (!drm_dev_enter(drm, &idx)) return -ENODEV; + /* Doubled for frame packing stereo modes */ + drm_mode_get_hv_timing(mode, &hdisplay, &vdisplay); + HVS_WRITE(SCALER_DISPCTRLX(chan), 0); HVS_WRITE(SCALER_DISPCTRLX(chan), SCALER_DISPCTRLX_RESET); HVS_WRITE(SCALER_DISPCTRLX(chan), 0); @@ -967,16 +971,16 @@ static int vc4_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, dispbkgndx = HVS_READ(SCALER_DISPBKGNDX(chan)); if (vc4->gen == VC4_GEN_4) { - dispctrl |= VC4_SET_FIELD(mode->hdisplay, + dispctrl |= VC4_SET_FIELD(hdisplay, SCALER_DISPCTRLX_WIDTH) | - VC4_SET_FIELD(mode->vdisplay, + VC4_SET_FIELD(vdisplay, SCALER_DISPCTRLX_HEIGHT) | (oneshot ? SCALER_DISPCTRLX_ONESHOT : 0); dispbkgndx |= SCALER_DISPBKGND_AUTOHS; } else { - dispctrl |= VC4_SET_FIELD(mode->hdisplay, + dispctrl |= VC4_SET_FIELD(hdisplay, SCALER5_DISPCTRLX_WIDTH) | - VC4_SET_FIELD(mode->vdisplay, + VC4_SET_FIELD(vdisplay, SCALER5_DISPCTRLX_HEIGHT) | (oneshot ? SCALER5_DISPCTRLX_ONESHOT : 0); dispbkgndx &= ~SCALER5_DISPBKGND_BCK2BCK; @@ -1009,6 +1013,7 @@ static int vc6_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, struct vc4_crtc_state *vc4_crtc_state = to_vc4_crtc_state(crtc->state); unsigned int chan = vc4_crtc_state->assigned_channel; bool interlace = mode->flags & DRM_MODE_FLAG_INTERLACE; + int hdisplay, vdisplay; u32 disp_ctrl1; int idx; @@ -1017,6 +1022,9 @@ static int vc6_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, if (!drm_dev_enter(drm, &idx)) return -ENODEV; + /* Doubled for frame packing stereo modes */ + drm_mode_get_hv_timing(mode, &hdisplay, &vdisplay); + HVS_WRITE(SCALER6_DISPX_CTRL0(chan), SCALER6_DISPX_CTRL0_RESET); disp_ctrl1 = HVS_READ(SCALER6_DISPX_CTRL1(chan)); @@ -1026,10 +1034,10 @@ static int vc6_hvs_init_channel(struct vc4_hvs *hvs, struct drm_crtc *crtc, HVS_WRITE(SCALER6_DISPX_CTRL0(chan), SCALER6_DISPX_CTRL0_ENB | - VC4_SET_FIELD(mode->hdisplay - 1, + VC4_SET_FIELD(hdisplay - 1, SCALER6_DISPX_CTRL0_FWIDTH) | (oneshot ? SCALER6_DISPX_CTRL0_ONESHOT : 0) | - VC4_SET_FIELD(mode->vdisplay - 1, + VC4_SET_FIELD(vdisplay - 1, SCALER6_DISPX_CTRL0_LINES)); drm_dev_exit(idx); From cbaa320679333a6813b01c24d05e1c798c67c9cb Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Mon, 27 Jul 2026 14:32:14 +0100 Subject: [PATCH 4/4] drm/vc4: Support frame packed stereo modes vc4 has always set connector->stereo_allowed, so a client with DRM_CLIENT_CAP_STEREO_3D is offered the frame packing modes advertised by the sink, but nothing ever expanded the timings for them. The pixelvalve was programmed with the base 2D raster while the sink expected two eyes and an active space gap, so the picture was wrong. Ask for CRTC_STEREO_DOUBLE when recomputing the adjusted mode's crtc_* fields. drm_mode_set_crtcinfo() then doubles crtc_vdisplay, crtc_vsync_start, crtc_vsync_end, crtc_vtotal and crtc_clock, which is everything the pixelvalve, the HVS and the audio CTS calculation need. The matching TMDS character rate comes from the HDMI connector helpers and the 3D_Structure vendor infoframe from drm_hdmi_vendor_infoframe_from_display_mode(), so nothing else has to change. CRTC_INTERLACE_HALVE_V has to be passed as well: drm_mode_set_crtcinfo() rebuilds every crtc_* field from scratch, and without it the halved field timings the core derived earlier for interlaced modes would be lost. Non-stereo modes are unaffected, as the CRTC_STEREO_DOUBLE adjustment applies only to modes carrying DRM_MODE_FLAG_3D_FRAME_PACKING. The WiFi interference workaround just above deliberately keeps keying off mode->clock, i.e. the 2D pixel clock. Frame packing modes landing in the affected range are not known to exist, and reworking that test would change behaviour for existing 2D modes. Tested on a Raspberry Pi 5. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_hdmi.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c index 78c52f8a0cb9a3..5833a6997fd66b 100644 --- a/drivers/gpu/drm/vc4/vc4_hdmi.c +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c @@ -1730,6 +1730,9 @@ static int vc4_hdmi_encoder_atomic_check(struct drm_encoder *encoder, tmds_char_rate = mode->clock * 1000; } + /* Rebuilds every crtc_* field, so CRTC_INTERLACE_HALVE_V is needed too */ + drm_mode_set_crtcinfo(mode, CRTC_INTERLACE_HALVE_V | CRTC_STEREO_DOUBLE); + return 0; }