* [PATCH v2 00/27] VRR Target Rate Limiter KMS uAPI and Implementation
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
This series defines a proposed new userspace API for specifying frame
rate targets for variable refresh rate (VRR) mechanisms. This includes
specifying both a constrained range in which VRR should operate, as well
as a fixed target, and a new connector property for QMS (Quick Media
Switching) and passive VRR (as in, keeping the display in VRR mode
without doing VRR).
The reason for wanting such an uAPI is mainly to avoid the imperfection
of the real world. By constraining VRR to a limited range or a fixed
point entirely, we get the benefits of judder-free (or low-latency)
presentation for content within an expected range of frame rates,
without dipping into ones that cause visible brightness flicker. The
addition of QMS furthermore allows the connector to signal an upcoming
rate change ahead of time for media playback scenarios, such that
advanced processing algorithms in e.g. TVs don't have to be disabled to
meet the need to be able to present at full rates with no notice.
Aside from the uAPI, there's the EDID parsing and an implementation for
drivers that use the common HDMI state helpers, with it wired up for
Rockchip (tested and developed on RK3588).
The uAPI consists of 4 new CRTC properties for the frame rate
minimum/maximum target, and several new connector properties. Two of the
connector properties are for reporting the display's VRR range to any
client so that they're aware of quirks the kernel chooses to override
the ones in the EDID with, and the third is for explicitly setting a
mode the display should be in (Game, which is default, Off, and Media).
The Media VRR display mode sets HDMI displays to use QMS. Whether or not
a particular display supports QMS can be determined from the EDID, i.e.
the kernel does not act as userspace's EDID parser through a redundant
property for this.
For some background on why the uAPI uses numerator/denominator frame
rate pairs as opposed to frame intervals or microhertz, my slides from
Display Next Hackfest 2026 give an explanation:
https://gitlab.freedesktop.org/-/project/2891/uploads/9db8886701e2598271a8c4c6dc4dc2b1/display_next_hackfest_2026.pdf
In short: for QMS, we have a very limited set of frame rate targets that
signalling is supported for. This includes both rates such as 24 Hz, and
24/1.001 Hz. Making userspace give us a frame rate as a rational
expression rather than as the output of some rounding calculations
spares us the pain and suffering of then having to correctly guess which
frame rate was originally meant as a target with a <0.1% accuracy
requirement.
There is also a set of IGT tests for exercising the new uAPI on actual
hardware:
https://gitlab.freedesktop.org/CounterPillow/igt-gpu-tools/-/tree/vrr-limiter
== Currently known deficiencies as of v2 ==
I've decided to send out a new revision now rather than wait longer, in
order to get more feedback on the approach. There's still some stuff
that isn't quite right or there yet though:
1. The immutable VRR flavour property for userspace isn't in yet, but
this will be added in the next revision; it's not relevant for the
general approach.
2. The EMP packet's m_const flag is not generated correctly for QMS.
Doing more commits skips without making changes to the connector
state skips the connector check, so it doesn't get updated to tick
over from m_const=0 to m_const=1 in a QMS tfr change scenario.
3. On my particular hardware, using modes for QMS that require TMDS
scrambling, with QMS rates that are quite low, results in a black
picture despite the display entering QMS mode. The IGT test contains
a workaround for this.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
Changes in v2:
- Make mode_changed on vrr_enabled change a driver-specific
consideration, and do it for Rockchip after verifying it's needed
- Move CRTC VRR limiter property attachment to separate function, to be
invoked by driver
- Invoke aforementioned function in Rockchip VOP2 driver for RK3588
- Validate target range completeness in common CRTC atomic_check code
- Move early hdmi_validate_vrr !vrr_enabled bailout to before state
re-use check
- Move addition of state re-use check's check for vrr target members to
the patch that actually implements them (d'oh)
- Make sure old_crtc_state in hdmi_validate_vrr is non-null before
dereferencing it
- Fix KUnit tests not working unless there was a mode_changed on
vrr_enabled change by explicitly getting a new connector state in each
test, refactoring all this out into a small function to call in each
- Use guard macro to acquire and release the modeset lock in KUnit
tests, fixing a leak of a held lock when tests fail
- Move added edids to drm_kunit_edid.c instead of defining them in the
header
- Add KUnit tests for limiter range incompleteness
- Deduplicate HDMI VRR EDID parsing with what already landed in drm-tip
and I rebased across by accident. Includes moving from monitor_range
to hdmi.vrr_cap.vrr_{min,max}
- Minor doc fix for the aforementioned already landed stuff
- Add vrr_display_min/vrr_display_max connector properties, including
HDMI helper to set them appropriately
- Add vrr_display_mode property for userspace to explicitly control the
VRR mode a display is in, in order to avoid blanking. Defaults to
"Game".
- Enable aforementioned connector properties for Rockchip dw_hdmi_qp
- Change qms_enabled to be just another enum value in vrr_display_mode
- Get rid of the VIC field, read that from the avi infoframe in EMP
generation instead
- Remove the memcpy prior state path in hdmi_validate_vrr
- Redo EMP generation with respect to new mode logic, remove some
redundancy
- Clear EMP infoframe regardless of sink-side support
- In hdmi_validate_vrr_limits, round up the vtotal check based on
vtotal remainder
- Support edge case of sinks that only support QMS, and not VRR,
and extend the QMS tests for this with another EDID
- In drm_crtc_helper_vrr_vsync, make the cur_err >= denom check
explicitly cast denom (u32) to int after dividing by 2, to avoid
both the bad u32 promotion of cur_err and the *2 wraparound
- In hdmi_validate_vrr, reset vrr state error accumulator if VRR target
changes
- In hdmi_validate_vrr, check for NULL old_conn_state before accessing
it for the QMS tfr byte check
- Drop the patch for disabling VRR in helper_set_config, as it'd regress
Xorg.
- Link to v1: https://patch.msgid.link/20260921-vrr-limiter-uapi-v1-0-2fcd7d011646@collabora.com
---
Derek Foreman (4):
video/hdmi: Add VTEM EMP packing
drm/bridge: Add VTEM EMP support
drm/connector: hdmi: Add VTEM EMP generation
drm/bridge: synopsys: Add VTEM EMP support
Nicolas Frattaroli (23):
drm: Add VRR state
drm/connector: add vrr_display_mode connector property
drm/crtc-helper: Add VRR helper functions
drm/connector: Add drm_display_info_is_vrr_capable
drm: Add VRR target frame rate properties
drm/connector: Add vrr_display_{min,max} connector properties
drm: Implement VRR rate limiting
drm/rockchip: dw_hdmi_qp: Add VRR support
drm/rockchip: Set mode_changed on vrr_enabled change
drm/rockchip: vop2: Enable VRR
drm/edid: Parse QMS flag from HDMI SCDS
drm/edid: Parse QMS TFR min/max flags from HDMI SCDS
drm/connector: Add "Media" VRR display mode for QMS
video/hdmi: Add support for QMS in VTEM EMP packing
drm/connector: hdmi: Add QMS to VTEM EMP generation
drm/connector: hdmi: Add QMS state validation and computation
drm/rockchip: dw_hdmi_qp: Add QMS support
drm/tests: hdmi: Add "Game Mode" VRR tests
drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
drm/tests: hdmi: Add Quick Media Switching tests
drm/connector: Fix docs for cnmvrr and cinemavrr members
drm/debugfs: Expose HDMI VRR in vrr_range debugfs
drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 50 +-
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h | 1 +
drivers/gpu/drm/display/drm_bridge_connector.c | 41 +
drivers/gpu/drm/display/drm_hdmi_helper.c | 55 +
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 488 ++++++++
drivers/gpu/drm/drm_atomic.c | 14 +
drivers/gpu/drm/drm_atomic_uapi.c | 28 +
drivers/gpu/drm/drm_connector.c | 217 ++++
drivers/gpu/drm/drm_crtc.c | 34 +
drivers/gpu/drm/drm_crtc_helper.c | 93 ++
drivers/gpu/drm/drm_debugfs.c | 24 +-
drivers/gpu/drm/drm_edid.c | 7 +
drivers/gpu/drm/drm_mode_config.c | 20 +
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 33 +
drivers/gpu/drm/rockchip/rockchip_drm_fb.c | 20 +-
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 65 +-
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 10 +
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 4 +
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 1308 ++++++++++++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.c | 420 +++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 3 +
drivers/video/hdmi.c | 99 +-
include/drm/display/drm_hdmi_helper.h | 3 +
include/drm/drm_bridge.h | 29 +
include/drm/drm_connector.h | 107 +-
include/drm/drm_crtc.h | 19 +
include/drm/drm_crtc_helper.h | 6 +
include/drm/drm_edid.h | 3 +
include/drm/drm_mode_config.h | 45 +
include/linux/hdmi.h | 21 +
30 files changed, 3245 insertions(+), 22 deletions(-)
---
base-commit: 0e8b43b5a5ef2dd52c454158e7fd8a927d796eb4
change-id: 20260919-vrr-limiter-uapi-c99ed93fe7d2
Best regards,
--
Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* [PATCH v2 00/27] VRR Target Rate Limiter KMS uAPI and Implementation
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
This series defines a proposed new userspace API for specifying frame
rate targets for variable refresh rate (VRR) mechanisms. This includes
specifying both a constrained range in which VRR should operate, as well
as a fixed target, and a new connector property for QMS (Quick Media
Switching) and passive VRR (as in, keeping the display in VRR mode
without doing VRR).
The reason for wanting such an uAPI is mainly to avoid the imperfection
of the real world. By constraining VRR to a limited range or a fixed
point entirely, we get the benefits of judder-free (or low-latency)
presentation for content within an expected range of frame rates,
without dipping into ones that cause visible brightness flicker. The
addition of QMS furthermore allows the connector to signal an upcoming
rate change ahead of time for media playback scenarios, such that
advanced processing algorithms in e.g. TVs don't have to be disabled to
meet the need to be able to present at full rates with no notice.
Aside from the uAPI, there's the EDID parsing and an implementation for
drivers that use the common HDMI state helpers, with it wired up for
Rockchip (tested and developed on RK3588).
The uAPI consists of 4 new CRTC properties for the frame rate
minimum/maximum target, and several new connector properties. Two of the
connector properties are for reporting the display's VRR range to any
client so that they're aware of quirks the kernel chooses to override
the ones in the EDID with, and the third is for explicitly setting a
mode the display should be in (Game, which is default, Off, and Media).
The Media VRR display mode sets HDMI displays to use QMS. Whether or not
a particular display supports QMS can be determined from the EDID, i.e.
the kernel does not act as userspace's EDID parser through a redundant
property for this.
For some background on why the uAPI uses numerator/denominator frame
rate pairs as opposed to frame intervals or microhertz, my slides from
Display Next Hackfest 2026 give an explanation:
https://gitlab.freedesktop.org/-/project/2891/uploads/9db8886701e2598271a8c4c6dc4dc2b1/display_next_hackfest_2026.pdf
In short: for QMS, we have a very limited set of frame rate targets that
signalling is supported for. This includes both rates such as 24 Hz, and
24/1.001 Hz. Making userspace give us a frame rate as a rational
expression rather than as the output of some rounding calculations
spares us the pain and suffering of then having to correctly guess which
frame rate was originally meant as a target with a <0.1% accuracy
requirement.
There is also a set of IGT tests for exercising the new uAPI on actual
hardware:
https://gitlab.freedesktop.org/CounterPillow/igt-gpu-tools/-/tree/vrr-limiter
== Currently known deficiencies as of v2 ==
I've decided to send out a new revision now rather than wait longer, in
order to get more feedback on the approach. There's still some stuff
that isn't quite right or there yet though:
1. The immutable VRR flavour property for userspace isn't in yet, but
this will be added in the next revision; it's not relevant for the
general approach.
2. The EMP packet's m_const flag is not generated correctly for QMS.
Doing more commits skips without making changes to the connector
state skips the connector check, so it doesn't get updated to tick
over from m_const=0 to m_const=1 in a QMS tfr change scenario.
3. On my particular hardware, using modes for QMS that require TMDS
scrambling, with QMS rates that are quite low, results in a black
picture despite the display entering QMS mode. The IGT test contains
a workaround for this.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
Changes in v2:
- Make mode_changed on vrr_enabled change a driver-specific
consideration, and do it for Rockchip after verifying it's needed
- Move CRTC VRR limiter property attachment to separate function, to be
invoked by driver
- Invoke aforementioned function in Rockchip VOP2 driver for RK3588
- Validate target range completeness in common CRTC atomic_check code
- Move early hdmi_validate_vrr !vrr_enabled bailout to before state
re-use check
- Move addition of state re-use check's check for vrr target members to
the patch that actually implements them (d'oh)
- Make sure old_crtc_state in hdmi_validate_vrr is non-null before
dereferencing it
- Fix KUnit tests not working unless there was a mode_changed on
vrr_enabled change by explicitly getting a new connector state in each
test, refactoring all this out into a small function to call in each
- Use guard macro to acquire and release the modeset lock in KUnit
tests, fixing a leak of a held lock when tests fail
- Move added edids to drm_kunit_edid.c instead of defining them in the
header
- Add KUnit tests for limiter range incompleteness
- Deduplicate HDMI VRR EDID parsing with what already landed in drm-tip
and I rebased across by accident. Includes moving from monitor_range
to hdmi.vrr_cap.vrr_{min,max}
- Minor doc fix for the aforementioned already landed stuff
- Add vrr_display_min/vrr_display_max connector properties, including
HDMI helper to set them appropriately
- Add vrr_display_mode property for userspace to explicitly control the
VRR mode a display is in, in order to avoid blanking. Defaults to
"Game".
- Enable aforementioned connector properties for Rockchip dw_hdmi_qp
- Change qms_enabled to be just another enum value in vrr_display_mode
- Get rid of the VIC field, read that from the avi infoframe in EMP
generation instead
- Remove the memcpy prior state path in hdmi_validate_vrr
- Redo EMP generation with respect to new mode logic, remove some
redundancy
- Clear EMP infoframe regardless of sink-side support
- In hdmi_validate_vrr_limits, round up the vtotal check based on
vtotal remainder
- Support edge case of sinks that only support QMS, and not VRR,
and extend the QMS tests for this with another EDID
- In drm_crtc_helper_vrr_vsync, make the cur_err >= denom check
explicitly cast denom (u32) to int after dividing by 2, to avoid
both the bad u32 promotion of cur_err and the *2 wraparound
- In hdmi_validate_vrr, reset vrr state error accumulator if VRR target
changes
- In hdmi_validate_vrr, check for NULL old_conn_state before accessing
it for the QMS tfr byte check
- Drop the patch for disabling VRR in helper_set_config, as it'd regress
Xorg.
- Link to v1: https://patch.msgid.link/20260921-vrr-limiter-uapi-v1-0-2fcd7d011646@collabora.com
---
Derek Foreman (4):
video/hdmi: Add VTEM EMP packing
drm/bridge: Add VTEM EMP support
drm/connector: hdmi: Add VTEM EMP generation
drm/bridge: synopsys: Add VTEM EMP support
Nicolas Frattaroli (23):
drm: Add VRR state
drm/connector: add vrr_display_mode connector property
drm/crtc-helper: Add VRR helper functions
drm/connector: Add drm_display_info_is_vrr_capable
drm: Add VRR target frame rate properties
drm/connector: Add vrr_display_{min,max} connector properties
drm: Implement VRR rate limiting
drm/rockchip: dw_hdmi_qp: Add VRR support
drm/rockchip: Set mode_changed on vrr_enabled change
drm/rockchip: vop2: Enable VRR
drm/edid: Parse QMS flag from HDMI SCDS
drm/edid: Parse QMS TFR min/max flags from HDMI SCDS
drm/connector: Add "Media" VRR display mode for QMS
video/hdmi: Add support for QMS in VTEM EMP packing
drm/connector: hdmi: Add QMS to VTEM EMP generation
drm/connector: hdmi: Add QMS state validation and computation
drm/rockchip: dw_hdmi_qp: Add QMS support
drm/tests: hdmi: Add "Game Mode" VRR tests
drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
drm/tests: hdmi: Add Quick Media Switching tests
drm/connector: Fix docs for cnmvrr and cinemavrr members
drm/debugfs: Expose HDMI VRR in vrr_range debugfs
drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 50 +-
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h | 1 +
drivers/gpu/drm/display/drm_bridge_connector.c | 41 +
drivers/gpu/drm/display/drm_hdmi_helper.c | 55 +
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 488 ++++++++
drivers/gpu/drm/drm_atomic.c | 14 +
drivers/gpu/drm/drm_atomic_uapi.c | 28 +
drivers/gpu/drm/drm_connector.c | 217 ++++
drivers/gpu/drm/drm_crtc.c | 34 +
drivers/gpu/drm/drm_crtc_helper.c | 93 ++
drivers/gpu/drm/drm_debugfs.c | 24 +-
drivers/gpu/drm/drm_edid.c | 7 +
drivers/gpu/drm/drm_mode_config.c | 20 +
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 33 +
drivers/gpu/drm/rockchip/rockchip_drm_fb.c | 20 +-
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 65 +-
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 10 +
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 4 +
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 1308 ++++++++++++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.c | 420 +++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 3 +
drivers/video/hdmi.c | 99 +-
include/drm/display/drm_hdmi_helper.h | 3 +
include/drm/drm_bridge.h | 29 +
include/drm/drm_connector.h | 107 +-
include/drm/drm_crtc.h | 19 +
include/drm/drm_crtc_helper.h | 6 +
include/drm/drm_edid.h | 3 +
include/drm/drm_mode_config.h | 45 +
include/linux/hdmi.h | 21 +
30 files changed, 3245 insertions(+), 22 deletions(-)
---
base-commit: 0e8b43b5a5ef2dd52c454158e7fd8a927d796eb4
change-id: 20260919-vrr-limiter-uapi-c99ed93fe7d2
Best regards,
--
Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply [flat|nested] 74+ messages in thread
* [PATCH v2 01/27] drm: Add VRR state
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add preliminary state tracking for VRR, and VRR state validation. This
will be used for generating VTEM infoframes in order to support variable
refresh rate functionality.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 208 ++++++++++++++++++++++++
include/drm/drm_crtc.h | 10 ++
2 files changed, 218 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index c63d439382bb..eb6abfd1350d 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -887,6 +887,210 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
return 0;
}
+/**
+ * cmp_fraction - compares two fractional numbers
+ * @a: numerator of the first fraction
+ * @b: denominator of the first fraction
+ * @c: numerator of the second fraction
+ * @d: denominator of the second fraction
+ *
+ * Compare fractional expression given by @a divided by @b with fractional
+ * expression given by @c divided by @d, without running into rounding issues.
+ *
+ * Neither @b nor @d should be 0.
+ *
+ * Returns:
+ * - %1 if @a / @b > @c / @d
+ * - %-1 if @a / @b < @c / @d
+ * - %0 if @a / @b == @c / @d
+ * - %-EDOM if @b is zero or @d is zero.
+ */
+static int cmp_fraction(u32 a, u32 b, u32 c, u32 d)
+{
+ u64 a_d = mul_u32_u32(a, d);
+ u64 b_c = mul_u32_u32(b, c);
+
+ if (WARN_ON(!b || !d))
+ return -EDOM;
+
+ /* a/b > c/d */
+ if (a_d > b_c)
+ return 1;
+
+ /* a/b < c/d */
+ if (a_d < b_c)
+ return -1;
+
+ /* a/b == c/d */
+ return 0;
+}
+
+static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
+ u32 rate_n, u32 rate_d, u16 *out_vtotal,
+ u32 *out_denom, u32 *out_frame_err)
+{
+ u32 denom, result, err;
+ u64 numerator;
+
+ /*
+ * We perform VRR based rate limiting by adjusting the vertical front
+ * porch. We do that by picking a new Vtotal to pass to the hardware,
+ * which extends the display period. Since we can change no other
+ * timings, this rarely results in a perfect integer match to the
+ * target framerate. However, we would like to correct for this error
+ * using only integer math.
+ *
+ * For a given refresh rate:
+ *
+ * Vtotal = clock
+ * ----------------------------------
+ * Htotal * target_refresh
+ *
+ * We specify target_refresh as a ratio, because the possible target
+ * framerates include rates like 23.97... that are actually 24 / 1.001,
+ * or 24000 / 1001.
+ *
+ * clock
+ * Vtotal = ----------------------------------
+ * Htotal * target_n / target_d
+ *
+ * Some algebra brings us to:
+ *
+ * clock * target_n
+ * Vtotal = ----------------------------------
+ * Htotal * rate_n
+ *
+ * The amount of timing error we accumulate every frame will be
+ * the remainder of that division.
+ *
+ * We store the remainder for later. Every frame we add the remainder
+ * to an accumulator, and when the accumulator exceeds the denominator,
+ * one Htotal worth of error has accumulated.
+ *
+ * In practice, as soon as the accumulated error exceeds half the
+ * denominator, it is dithered across frames by temporarily extending
+ * Vtotal by a single row on that frame.
+ */
+
+ if (check_mul_overflow(mode->crtc_htotal, rate_n, &denom))
+ return -ERANGE;
+
+ if (check_mul_overflow(mode->crtc_clock * 1000ULL, rate_d, &numerator))
+ return -ERANGE;
+
+ result = div_u64_rem(numerator, denom, &err);
+ if (result > U16_MAX)
+ return -ERANGE;
+
+ if (out_vtotal)
+ *out_vtotal = result;
+ if (out_denom)
+ *out_denom = denom;
+ if (out_frame_err)
+ *out_frame_err = err;
+
+ return 0;
+}
+
+static int hdmi_validate_vrr(struct drm_connector *connector,
+ struct drm_atomic_commit *state)
+{
+ struct drm_connector_state *new_conn_state =
+ drm_atomic_get_new_connector_state(state, connector);
+ struct drm_crtc_state *new_crtc_state =
+ drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
+ struct drm_crtc_vrr_state *vrr_state =
+ &new_crtc_state->vrr_state;
+ struct drm_display_info *info = &connector->display_info;
+ const struct drm_display_mode *mode = &new_crtc_state->mode;
+ struct drm_device *dev = connector->dev;
+ int mode_refresh, vfront, ret;
+ u32 min_vfreq_n = info->hdmi.vrr_cap.vrr_min;
+ u32 min_vfreq_d = 1;
+ u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
+ u32 max_vfreq_d = 1;
+
+ if (!new_crtc_state->vrr_enabled)
+ return 0;
+
+ if (!info->hdmi.vrr_cap.supported)
+ return -EOPNOTSUPP;
+
+ mode_refresh = drm_mode_vrefresh(mode);
+
+ /* HDMI uses 10 bits to signal the base refresh. */
+ if (mode_refresh < 0 || mode_refresh > 1023) {
+ drm_dbg_kms(dev, "Mode's refresh of %dHz > HDMI VRR maximum (1023)\n",
+ mode_refresh);
+ return -EINVAL;
+ }
+
+ if (!max_vfreq_n || max_vfreq_n > mode_refresh)
+ max_vfreq_n = mode_refresh;
+
+ if (!min_vfreq_n) {
+ drm_dbg_kms(dev, "Could not get minimum VRR rate from sink\n");
+ return -EINVAL;
+ }
+
+ vfront = mode->crtc_vsync_start - mode->crtc_vdisplay;
+ if (vfront < 0 || vfront > U8_MAX) {
+ drm_dbg_kms(dev, "Vfront of %d would not fit in VTEM packet\n", vfront);
+ return -EINVAL;
+ }
+
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
+ max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+
+ ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
+ &vrr_state->base_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ /*
+ * Even at its fastest, it can't go faster than the mode, so
+ * clamp to avoid imprecisely rounded mode_refresh values we
+ * can't do anything about from ruining our day.
+ */
+ vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
+
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
+ /* refresh <= VRR min, don't do VRR vtotal adjustment */
+ vrr_state->max_vtotal = mode->crtc_vtotal;
+ } else {
+ /*
+ * Allow additional front porch until effective rate == VRR min
+ * In essence, this is mode->vtotal * mode_refresh / min_vfreq
+ * but the relevant factors factored out from drm_mode_vrefresh()
+ * to avoid working with rounded values, as well as using the
+ * hardware adjusted crtc_* values instead.
+ */
+ ret = hdmi_calculate_vtotal(mode, min_vfreq_n, min_vfreq_d,
+ &vrr_state->max_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate max_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ if (vrr_state->max_vtotal < mode->crtc_vtotal) {
+ drm_dbg_kms(dev, "max_vtotal=%u < crtc_vtotal=%u\n",
+ vrr_state->max_vtotal, mode->crtc_vtotal);
+ return -EINVAL;
+ }
+ }
+
+ drm_dbg_kms(dev, "VRR has base_vtotal=%u max_vtotal=%u from %u/%uHz <= rate <= %u/%uHz\n",
+ vrr_state->base_vtotal, vrr_state->max_vtotal, min_vfreq_n,
+ min_vfreq_d, max_vfreq_n, max_vfreq_d);
+
+ return 0;
+}
+
/**
* drm_atomic_helper_connector_hdmi_check() - Helper to check HDMI connector atomic state
* @connector: DRM Connector
@@ -919,6 +1123,10 @@ int drm_atomic_helper_connector_hdmi_check(struct drm_connector *connector,
new_conn_state->hdmi.is_limited_range = hdmi_is_limited_range(connector, new_conn_state);
+ ret = hdmi_validate_vrr(connector, state);
+ if (ret)
+ return ret;
+
ret = hdmi_generate_infoframes(connector, new_conn_state);
if (ret)
return ret;
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index be5dca538d87..bfb2b0741625 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -63,6 +63,13 @@ struct drm_atomic_commit;
struct drm_crtc_helper_funcs;
struct drm_plane_helper_funcs;
+struct drm_crtc_vrr_state {
+ u16 cur_vtotal;
+ u16 max_vtotal;
+ u16 base_vtotal;
+ bool dynamic;
+};
+
/**
* struct drm_crtc_state - mutable CRTC state
*
@@ -397,6 +404,9 @@ struct drm_crtc_state {
*/
struct drm_pending_vblank_event *event;
+ /** @vrr_state: State related to variable refresh rate. */
+ struct drm_crtc_vrr_state vrr_state;
+
/**
* @commit:
*
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 01/27] drm: Add VRR state
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add preliminary state tracking for VRR, and VRR state validation. This
will be used for generating VTEM infoframes in order to support variable
refresh rate functionality.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 208 ++++++++++++++++++++++++
include/drm/drm_crtc.h | 10 ++
2 files changed, 218 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index c63d439382bb..eb6abfd1350d 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -887,6 +887,210 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
return 0;
}
+/**
+ * cmp_fraction - compares two fractional numbers
+ * @a: numerator of the first fraction
+ * @b: denominator of the first fraction
+ * @c: numerator of the second fraction
+ * @d: denominator of the second fraction
+ *
+ * Compare fractional expression given by @a divided by @b with fractional
+ * expression given by @c divided by @d, without running into rounding issues.
+ *
+ * Neither @b nor @d should be 0.
+ *
+ * Returns:
+ * - %1 if @a / @b > @c / @d
+ * - %-1 if @a / @b < @c / @d
+ * - %0 if @a / @b == @c / @d
+ * - %-EDOM if @b is zero or @d is zero.
+ */
+static int cmp_fraction(u32 a, u32 b, u32 c, u32 d)
+{
+ u64 a_d = mul_u32_u32(a, d);
+ u64 b_c = mul_u32_u32(b, c);
+
+ if (WARN_ON(!b || !d))
+ return -EDOM;
+
+ /* a/b > c/d */
+ if (a_d > b_c)
+ return 1;
+
+ /* a/b < c/d */
+ if (a_d < b_c)
+ return -1;
+
+ /* a/b == c/d */
+ return 0;
+}
+
+static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
+ u32 rate_n, u32 rate_d, u16 *out_vtotal,
+ u32 *out_denom, u32 *out_frame_err)
+{
+ u32 denom, result, err;
+ u64 numerator;
+
+ /*
+ * We perform VRR based rate limiting by adjusting the vertical front
+ * porch. We do that by picking a new Vtotal to pass to the hardware,
+ * which extends the display period. Since we can change no other
+ * timings, this rarely results in a perfect integer match to the
+ * target framerate. However, we would like to correct for this error
+ * using only integer math.
+ *
+ * For a given refresh rate:
+ *
+ * Vtotal = clock
+ * ----------------------------------
+ * Htotal * target_refresh
+ *
+ * We specify target_refresh as a ratio, because the possible target
+ * framerates include rates like 23.97... that are actually 24 / 1.001,
+ * or 24000 / 1001.
+ *
+ * clock
+ * Vtotal = ----------------------------------
+ * Htotal * target_n / target_d
+ *
+ * Some algebra brings us to:
+ *
+ * clock * target_n
+ * Vtotal = ----------------------------------
+ * Htotal * rate_n
+ *
+ * The amount of timing error we accumulate every frame will be
+ * the remainder of that division.
+ *
+ * We store the remainder for later. Every frame we add the remainder
+ * to an accumulator, and when the accumulator exceeds the denominator,
+ * one Htotal worth of error has accumulated.
+ *
+ * In practice, as soon as the accumulated error exceeds half the
+ * denominator, it is dithered across frames by temporarily extending
+ * Vtotal by a single row on that frame.
+ */
+
+ if (check_mul_overflow(mode->crtc_htotal, rate_n, &denom))
+ return -ERANGE;
+
+ if (check_mul_overflow(mode->crtc_clock * 1000ULL, rate_d, &numerator))
+ return -ERANGE;
+
+ result = div_u64_rem(numerator, denom, &err);
+ if (result > U16_MAX)
+ return -ERANGE;
+
+ if (out_vtotal)
+ *out_vtotal = result;
+ if (out_denom)
+ *out_denom = denom;
+ if (out_frame_err)
+ *out_frame_err = err;
+
+ return 0;
+}
+
+static int hdmi_validate_vrr(struct drm_connector *connector,
+ struct drm_atomic_commit *state)
+{
+ struct drm_connector_state *new_conn_state =
+ drm_atomic_get_new_connector_state(state, connector);
+ struct drm_crtc_state *new_crtc_state =
+ drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
+ struct drm_crtc_vrr_state *vrr_state =
+ &new_crtc_state->vrr_state;
+ struct drm_display_info *info = &connector->display_info;
+ const struct drm_display_mode *mode = &new_crtc_state->mode;
+ struct drm_device *dev = connector->dev;
+ int mode_refresh, vfront, ret;
+ u32 min_vfreq_n = info->hdmi.vrr_cap.vrr_min;
+ u32 min_vfreq_d = 1;
+ u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
+ u32 max_vfreq_d = 1;
+
+ if (!new_crtc_state->vrr_enabled)
+ return 0;
+
+ if (!info->hdmi.vrr_cap.supported)
+ return -EOPNOTSUPP;
+
+ mode_refresh = drm_mode_vrefresh(mode);
+
+ /* HDMI uses 10 bits to signal the base refresh. */
+ if (mode_refresh < 0 || mode_refresh > 1023) {
+ drm_dbg_kms(dev, "Mode's refresh of %dHz > HDMI VRR maximum (1023)\n",
+ mode_refresh);
+ return -EINVAL;
+ }
+
+ if (!max_vfreq_n || max_vfreq_n > mode_refresh)
+ max_vfreq_n = mode_refresh;
+
+ if (!min_vfreq_n) {
+ drm_dbg_kms(dev, "Could not get minimum VRR rate from sink\n");
+ return -EINVAL;
+ }
+
+ vfront = mode->crtc_vsync_start - mode->crtc_vdisplay;
+ if (vfront < 0 || vfront > U8_MAX) {
+ drm_dbg_kms(dev, "Vfront of %d would not fit in VTEM packet\n", vfront);
+ return -EINVAL;
+ }
+
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
+ max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+
+ ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
+ &vrr_state->base_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ /*
+ * Even at its fastest, it can't go faster than the mode, so
+ * clamp to avoid imprecisely rounded mode_refresh values we
+ * can't do anything about from ruining our day.
+ */
+ vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
+
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
+ /* refresh <= VRR min, don't do VRR vtotal adjustment */
+ vrr_state->max_vtotal = mode->crtc_vtotal;
+ } else {
+ /*
+ * Allow additional front porch until effective rate == VRR min
+ * In essence, this is mode->vtotal * mode_refresh / min_vfreq
+ * but the relevant factors factored out from drm_mode_vrefresh()
+ * to avoid working with rounded values, as well as using the
+ * hardware adjusted crtc_* values instead.
+ */
+ ret = hdmi_calculate_vtotal(mode, min_vfreq_n, min_vfreq_d,
+ &vrr_state->max_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate max_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ if (vrr_state->max_vtotal < mode->crtc_vtotal) {
+ drm_dbg_kms(dev, "max_vtotal=%u < crtc_vtotal=%u\n",
+ vrr_state->max_vtotal, mode->crtc_vtotal);
+ return -EINVAL;
+ }
+ }
+
+ drm_dbg_kms(dev, "VRR has base_vtotal=%u max_vtotal=%u from %u/%uHz <= rate <= %u/%uHz\n",
+ vrr_state->base_vtotal, vrr_state->max_vtotal, min_vfreq_n,
+ min_vfreq_d, max_vfreq_n, max_vfreq_d);
+
+ return 0;
+}
+
/**
* drm_atomic_helper_connector_hdmi_check() - Helper to check HDMI connector atomic state
* @connector: DRM Connector
@@ -919,6 +1123,10 @@ int drm_atomic_helper_connector_hdmi_check(struct drm_connector *connector,
new_conn_state->hdmi.is_limited_range = hdmi_is_limited_range(connector, new_conn_state);
+ ret = hdmi_validate_vrr(connector, state);
+ if (ret)
+ return ret;
+
ret = hdmi_generate_infoframes(connector, new_conn_state);
if (ret)
return ret;
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index be5dca538d87..bfb2b0741625 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -63,6 +63,13 @@ struct drm_atomic_commit;
struct drm_crtc_helper_funcs;
struct drm_plane_helper_funcs;
+struct drm_crtc_vrr_state {
+ u16 cur_vtotal;
+ u16 max_vtotal;
+ u16 base_vtotal;
+ bool dynamic;
+};
+
/**
* struct drm_crtc_state - mutable CRTC state
*
@@ -397,6 +404,9 @@ struct drm_crtc_state {
*/
struct drm_pending_vblank_event *event;
+ /** @vrr_state: State related to variable refresh rate. */
+ struct drm_crtc_vrr_state vrr_state;
+
/**
* @commit:
*
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 02/27] drm/connector: add vrr_display_mode connector property
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
It's common to want to keep a VRR-capable display in a specific VRR mode
regardless of whether a CRTC uses VRR presentation, to avoid screen
blanking during switching of the VRR mode. On the other hand, it's also
not always desired to keep a display in VRR mode even when nothing does
VRR presentation, for example in battery-powered systems.
To let userspace control this explicitly, instead of relying on implicit
driver policy, introduce a new connector KMS property: vrr_display_mode.
It defaults to "Game", which matches the behaviour of dGPU drivers,
wherein a VRR-capable display will always be in Game mode. However, it
also has an "Off" mode, with which userspace can explicitly request that
VRR be disabled on the display sink.
The associated connector state member uses an u8 rather than the enum
type in order to fit into the existing 2 bytes of padding there. Once
the kernel mandates compiler versions that have backported C23 features,
the enum can be given an explicit small backing type and the member can
be changed to use the enum type.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_atomic_uapi.c | 4 ++
drivers/gpu/drm/drm_connector.c | 77 +++++++++++++++++++++++++++++++++++++++
include/drm/drm_connector.h | 29 +++++++++++++++
3 files changed, 110 insertions(+)
diff --git a/drivers/gpu/drm/drm_atomic_uapi.c b/drivers/gpu/drm/drm_atomic_uapi.c
index 1eebcf6f5e05..127bdd77802b 100644
--- a/drivers/gpu/drm/drm_atomic_uapi.c
+++ b/drivers/gpu/drm/drm_atomic_uapi.c
@@ -969,6 +969,8 @@ static int drm_atomic_connector_set_property(struct drm_connector *connector,
state->hdmi.broadcast_rgb = val;
} else if (property == connector->color_format_property) {
state->color_format = val;
+ } else if (property == connector->vrr_display_mode_property) {
+ state->vrr_display_mode = val;
} else if (connector->funcs->atomic_set_property) {
return connector->funcs->atomic_set_property(connector,
state, property, val);
@@ -1056,6 +1058,8 @@ drm_atomic_connector_get_property(struct drm_connector *connector,
*val = state->hdmi.broadcast_rgb;
} else if (property == connector->color_format_property) {
*val = state->color_format;
+ } else if (property == connector->vrr_display_mode_property) {
+ *val = state->vrr_display_mode;
} else if (connector->funcs->atomic_get_property) {
return connector->funcs->atomic_get_property(connector,
state, property, val);
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 893716a9e489..65c14b6e47a6 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1434,6 +1434,12 @@ static const struct drm_prop_enum_list drm_tv_mode_enum_list[] = {
};
DRM_ENUM_NAME_FN(drm_get_tv_mode_name, drm_tv_mode_enum_list)
+static const struct drm_prop_enum_list drm_vrr_mode_enum_list[] = {
+ { VRR_DISPLAY_MODE_GAME, "Game" },
+ { VRR_DISPLAY_MODE_OFF, "Off" },
+};
+DRM_ENUM_NAME_FN(drm_get_vrr_mode_name, drm_vrr_mode_enum_list)
+
/**
* drm_get_tv_mode_from_name - Translates a TV mode name into its enum value
* @name: TV Mode name we want to convert
@@ -2560,6 +2566,14 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
*
* The driver may place further restrictions within these minimum
* and maximum bounds.
+ *
+ * "vrr_display_mode":
+ * Optional &drm_connector enum property that controls which VRR mode, if
+ * any, a display is in. Possible values::
+ *
+ * - Game - set display to game mode if display supports it,
+ * regardless of CRTC state
+ * - Off - set display to VRR disabled mode, regardless of CRTC state
*/
/**
@@ -2593,6 +2607,69 @@ int drm_connector_attach_vrr_capable_property(
}
EXPORT_SYMBOL(drm_connector_attach_vrr_capable_property);
+/**
+ * drm_connector_attach_vrr_display_mode_property - creates and attaches VRR
+ * display mode property
+ * @connector: pointer to the &struct drm_connector to attach the property to
+ * @supported_vrr_modes: bitmask of VRR modes the connector supports regardless
+ * of sink
+ *
+ * Creates and attaches the "vrr_display_mode" property to @connector. The
+ * property must not already exist on it. @supported_vrr_modes is a bitmask
+ * wherein BIT(VRR_DISPLAY_MODE_FOO) indicates VRR_DISPLAY_MODE_FOO is supported
+ * by the connector.
+ *
+ * Returns:
+ * Zero on success, negative errno on failure.
+ */
+int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
+ unsigned int supported_vrr_modes)
+{
+ struct drm_device *dev = connector->dev;
+ struct drm_prop_enum_list enum_list[VRR_DISPLAY_MODE_COUNT];
+ unsigned int num_enums = 0;
+ struct drm_property *prop;
+ int i;
+
+ if (supported_vrr_modes & ~(BIT(VRR_DISPLAY_MODE_COUNT) - 1)) {
+ drm_err(dev, "Unknown VRR modes provided on [CONNECTOR:%d:%s]\n",
+ connector->base.id, connector->name);
+ return -EINVAL;
+ }
+
+ if (connector->vrr_display_mode_property) {
+ drm_err(dev, "[CONNECTOR:%d:%s] already has a 'vrr_display_mode' property\n",
+ connector->base.id, connector->name);
+ return -EEXIST;
+ }
+
+ if (!(supported_vrr_modes & BIT(VRR_DISPLAY_MODE_GAME))) {
+ drm_err(dev, "Missing default VRR mode on [CONNECTOR:%d:%s]\n",
+ connector->base.id, connector->name);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < VRR_DISPLAY_MODE_COUNT; i++) {
+ if (BIT(i) & supported_vrr_modes) {
+ enum_list[num_enums].name = drm_get_vrr_mode_name(i);
+ enum_list[num_enums].type = i;
+ num_enums++;
+ }
+ }
+
+ prop = drm_property_create_enum(dev, DRM_MODE_PROP_ENUM, "vrr_display_mode",
+ enum_list, num_enums);
+
+ if (!prop)
+ return -ENOMEM;
+
+ connector->vrr_display_mode_property = prop;
+ drm_object_attach_property(&connector->base, prop, VRR_DISPLAY_MODE_GAME);
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_attach_vrr_display_mode_property);
+
/**
* drm_connector_attach_scaling_mode_property - attach atomic scaling mode property
* @connector: connector to attach scaling mode property on.
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 2ee7c59329aa..8699f35cafcb 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -809,6 +809,15 @@ enum drm_bus_flags {
DRM_BUS_FLAG_SHARP_SIGNALS = BIT(8),
};
+enum vrr_display_mode {
+ /** @VRR_DISPLAY_MODE_GAME: Set the display to VRR "Game" mode */
+ VRR_DISPLAY_MODE_GAME = 0,
+ /** @VRR_DISPLAY_MODE_OFF: Turn off VRR on the display */
+ VRR_DISPLAY_MODE_OFF,
+ /** @VRR_DISPLAY_MODE_COUNT: Sentinel, not a valid mode, keep last */
+ VRR_DISPLAY_MODE_COUNT
+};
+
/**
* struct drm_amd_vsdb_info - AMD-specific VSDB information
*
@@ -1352,6 +1361,12 @@ struct drm_connector_state {
*/
u8 max_bpc;
+ /**
+ * @vrr_display_mode: One of &enum vrr_display_mode to indicate the VRR
+ * type to use on a VRR-capable display.
+ */
+ u8 vrr_display_mode;
+
/**
* @privacy_screen_sw_state: See :ref:`Standard Connector
* Properties<standard_connector_properties>`
@@ -2454,6 +2469,17 @@ struct drm_connector {
*/
struct drm_property *vrr_capable_property;
+ /**
+ * @vrr_display_mode_property: Optional property for userspace to set
+ * a connected display to a specific VRR mode, regardless of the
+ * connected CRTC's VRR_ENABLED property. Drivers can create and attach
+ * the property to a connector by calling
+ * drm_connector_attach_vrr_display_mode_property(). Implementations can
+ * then read the set display mode (&enum vrr_display_mode) from
+ * &drm_connector_state.vrr_display_mode.
+ */
+ struct drm_property *vrr_display_mode_property;
+
/**
* @colorspace_property: Connector property to set the suitable
* colorspace supported by the sink.
@@ -2859,6 +2885,7 @@ const char *drm_get_dpms_name(int val);
const char *drm_get_dvi_i_subconnector_name(int val);
const char *drm_get_dvi_i_select_name(int val);
const char *drm_get_tv_mode_name(int val);
+const char *drm_get_vrr_mode_name(int val);
const char *drm_get_tv_subconnector_name(int val);
const char *drm_get_tv_select_name(int val);
const char *drm_get_dp_subconnector_name(int val);
@@ -2883,6 +2910,8 @@ int drm_connector_attach_scaling_mode_property(struct drm_connector *connector,
u32 scaling_mode_mask);
int drm_connector_attach_vrr_capable_property(
struct drm_connector *connector);
+int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
+ unsigned int supported_vrr_modes);
void drm_connector_attach_panel_type_property(struct drm_connector *connector);
int drm_connector_attach_broadcast_rgb_property(struct drm_connector *connector);
int drm_connector_attach_colorspace_property(struct drm_connector *connector);
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 02/27] drm/connector: add vrr_display_mode connector property
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
It's common to want to keep a VRR-capable display in a specific VRR mode
regardless of whether a CRTC uses VRR presentation, to avoid screen
blanking during switching of the VRR mode. On the other hand, it's also
not always desired to keep a display in VRR mode even when nothing does
VRR presentation, for example in battery-powered systems.
To let userspace control this explicitly, instead of relying on implicit
driver policy, introduce a new connector KMS property: vrr_display_mode.
It defaults to "Game", which matches the behaviour of dGPU drivers,
wherein a VRR-capable display will always be in Game mode. However, it
also has an "Off" mode, with which userspace can explicitly request that
VRR be disabled on the display sink.
The associated connector state member uses an u8 rather than the enum
type in order to fit into the existing 2 bytes of padding there. Once
the kernel mandates compiler versions that have backported C23 features,
the enum can be given an explicit small backing type and the member can
be changed to use the enum type.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_atomic_uapi.c | 4 ++
drivers/gpu/drm/drm_connector.c | 77 +++++++++++++++++++++++++++++++++++++++
include/drm/drm_connector.h | 29 +++++++++++++++
3 files changed, 110 insertions(+)
diff --git a/drivers/gpu/drm/drm_atomic_uapi.c b/drivers/gpu/drm/drm_atomic_uapi.c
index 1eebcf6f5e05..127bdd77802b 100644
--- a/drivers/gpu/drm/drm_atomic_uapi.c
+++ b/drivers/gpu/drm/drm_atomic_uapi.c
@@ -969,6 +969,8 @@ static int drm_atomic_connector_set_property(struct drm_connector *connector,
state->hdmi.broadcast_rgb = val;
} else if (property == connector->color_format_property) {
state->color_format = val;
+ } else if (property == connector->vrr_display_mode_property) {
+ state->vrr_display_mode = val;
} else if (connector->funcs->atomic_set_property) {
return connector->funcs->atomic_set_property(connector,
state, property, val);
@@ -1056,6 +1058,8 @@ drm_atomic_connector_get_property(struct drm_connector *connector,
*val = state->hdmi.broadcast_rgb;
} else if (property == connector->color_format_property) {
*val = state->color_format;
+ } else if (property == connector->vrr_display_mode_property) {
+ *val = state->vrr_display_mode;
} else if (connector->funcs->atomic_get_property) {
return connector->funcs->atomic_get_property(connector,
state, property, val);
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 893716a9e489..65c14b6e47a6 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1434,6 +1434,12 @@ static const struct drm_prop_enum_list drm_tv_mode_enum_list[] = {
};
DRM_ENUM_NAME_FN(drm_get_tv_mode_name, drm_tv_mode_enum_list)
+static const struct drm_prop_enum_list drm_vrr_mode_enum_list[] = {
+ { VRR_DISPLAY_MODE_GAME, "Game" },
+ { VRR_DISPLAY_MODE_OFF, "Off" },
+};
+DRM_ENUM_NAME_FN(drm_get_vrr_mode_name, drm_vrr_mode_enum_list)
+
/**
* drm_get_tv_mode_from_name - Translates a TV mode name into its enum value
* @name: TV Mode name we want to convert
@@ -2560,6 +2566,14 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
*
* The driver may place further restrictions within these minimum
* and maximum bounds.
+ *
+ * "vrr_display_mode":
+ * Optional &drm_connector enum property that controls which VRR mode, if
+ * any, a display is in. Possible values::
+ *
+ * - Game - set display to game mode if display supports it,
+ * regardless of CRTC state
+ * - Off - set display to VRR disabled mode, regardless of CRTC state
*/
/**
@@ -2593,6 +2607,69 @@ int drm_connector_attach_vrr_capable_property(
}
EXPORT_SYMBOL(drm_connector_attach_vrr_capable_property);
+/**
+ * drm_connector_attach_vrr_display_mode_property - creates and attaches VRR
+ * display mode property
+ * @connector: pointer to the &struct drm_connector to attach the property to
+ * @supported_vrr_modes: bitmask of VRR modes the connector supports regardless
+ * of sink
+ *
+ * Creates and attaches the "vrr_display_mode" property to @connector. The
+ * property must not already exist on it. @supported_vrr_modes is a bitmask
+ * wherein BIT(VRR_DISPLAY_MODE_FOO) indicates VRR_DISPLAY_MODE_FOO is supported
+ * by the connector.
+ *
+ * Returns:
+ * Zero on success, negative errno on failure.
+ */
+int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
+ unsigned int supported_vrr_modes)
+{
+ struct drm_device *dev = connector->dev;
+ struct drm_prop_enum_list enum_list[VRR_DISPLAY_MODE_COUNT];
+ unsigned int num_enums = 0;
+ struct drm_property *prop;
+ int i;
+
+ if (supported_vrr_modes & ~(BIT(VRR_DISPLAY_MODE_COUNT) - 1)) {
+ drm_err(dev, "Unknown VRR modes provided on [CONNECTOR:%d:%s]\n",
+ connector->base.id, connector->name);
+ return -EINVAL;
+ }
+
+ if (connector->vrr_display_mode_property) {
+ drm_err(dev, "[CONNECTOR:%d:%s] already has a 'vrr_display_mode' property\n",
+ connector->base.id, connector->name);
+ return -EEXIST;
+ }
+
+ if (!(supported_vrr_modes & BIT(VRR_DISPLAY_MODE_GAME))) {
+ drm_err(dev, "Missing default VRR mode on [CONNECTOR:%d:%s]\n",
+ connector->base.id, connector->name);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < VRR_DISPLAY_MODE_COUNT; i++) {
+ if (BIT(i) & supported_vrr_modes) {
+ enum_list[num_enums].name = drm_get_vrr_mode_name(i);
+ enum_list[num_enums].type = i;
+ num_enums++;
+ }
+ }
+
+ prop = drm_property_create_enum(dev, DRM_MODE_PROP_ENUM, "vrr_display_mode",
+ enum_list, num_enums);
+
+ if (!prop)
+ return -ENOMEM;
+
+ connector->vrr_display_mode_property = prop;
+ drm_object_attach_property(&connector->base, prop, VRR_DISPLAY_MODE_GAME);
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_attach_vrr_display_mode_property);
+
/**
* drm_connector_attach_scaling_mode_property - attach atomic scaling mode property
* @connector: connector to attach scaling mode property on.
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 2ee7c59329aa..8699f35cafcb 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -809,6 +809,15 @@ enum drm_bus_flags {
DRM_BUS_FLAG_SHARP_SIGNALS = BIT(8),
};
+enum vrr_display_mode {
+ /** @VRR_DISPLAY_MODE_GAME: Set the display to VRR "Game" mode */
+ VRR_DISPLAY_MODE_GAME = 0,
+ /** @VRR_DISPLAY_MODE_OFF: Turn off VRR on the display */
+ VRR_DISPLAY_MODE_OFF,
+ /** @VRR_DISPLAY_MODE_COUNT: Sentinel, not a valid mode, keep last */
+ VRR_DISPLAY_MODE_COUNT
+};
+
/**
* struct drm_amd_vsdb_info - AMD-specific VSDB information
*
@@ -1352,6 +1361,12 @@ struct drm_connector_state {
*/
u8 max_bpc;
+ /**
+ * @vrr_display_mode: One of &enum vrr_display_mode to indicate the VRR
+ * type to use on a VRR-capable display.
+ */
+ u8 vrr_display_mode;
+
/**
* @privacy_screen_sw_state: See :ref:`Standard Connector
* Properties<standard_connector_properties>`
@@ -2454,6 +2469,17 @@ struct drm_connector {
*/
struct drm_property *vrr_capable_property;
+ /**
+ * @vrr_display_mode_property: Optional property for userspace to set
+ * a connected display to a specific VRR mode, regardless of the
+ * connected CRTC's VRR_ENABLED property. Drivers can create and attach
+ * the property to a connector by calling
+ * drm_connector_attach_vrr_display_mode_property(). Implementations can
+ * then read the set display mode (&enum vrr_display_mode) from
+ * &drm_connector_state.vrr_display_mode.
+ */
+ struct drm_property *vrr_display_mode_property;
+
/**
* @colorspace_property: Connector property to set the suitable
* colorspace supported by the sink.
@@ -2859,6 +2885,7 @@ const char *drm_get_dpms_name(int val);
const char *drm_get_dvi_i_subconnector_name(int val);
const char *drm_get_dvi_i_select_name(int val);
const char *drm_get_tv_mode_name(int val);
+const char *drm_get_vrr_mode_name(int val);
const char *drm_get_tv_subconnector_name(int val);
const char *drm_get_tv_select_name(int val);
const char *drm_get_dp_subconnector_name(int val);
@@ -2883,6 +2910,8 @@ int drm_connector_attach_scaling_mode_property(struct drm_connector *connector,
u32 scaling_mode_mask);
int drm_connector_attach_vrr_capable_property(
struct drm_connector *connector);
+int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
+ unsigned int supported_vrr_modes);
void drm_connector_attach_panel_type_property(struct drm_connector *connector);
int drm_connector_attach_broadcast_rgb_property(struct drm_connector *connector);
int drm_connector_attach_colorspace_property(struct drm_connector *connector);
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 03/27] video/hdmi: Add VTEM EMP packing
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
VTEM EMPs are a type of infoframe that is required for Variable Refresh
Rate. Add code to pack them.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/video/hdmi.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++-
include/linux/hdmi.h | 19 ++++++++++
2 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/drivers/video/hdmi.c b/drivers/video/hdmi.c
index 45b42f14a750..ed9abc7ad228 100644
--- a/drivers/video/hdmi.c
+++ b/drivers/video/hdmi.c
@@ -983,6 +983,10 @@ hdmi_infoframe_pack(union hdmi_infoframe *frame,
length = hdmi_vendor_any_infoframe_pack(&frame->vendor,
buffer, size);
break;
+ case HDMI_INFOFRAME_TYPE_EMP:
+ length = hdmi_emp_infoframe_vtem_pack(&frame->vtem,
+ buffer, size);
+ break;
default:
WARN(1, "Bad infoframe type %d\n", frame->any.type);
length = -EINVAL;
@@ -994,9 +998,11 @@ EXPORT_SYMBOL(hdmi_infoframe_pack);
static const char *hdmi_infoframe_type_get_name(enum hdmi_infoframe_type type)
{
- if (type < 0x80 || type > 0x9f)
+ if (type < 0x7F || type > 0x9f)
return "Invalid";
switch (type) {
+ case HDMI_INFOFRAME_TYPE_EMP:
+ return "EMP";
case HDMI_INFOFRAME_TYPE_VENDOR:
return "Vendor";
case HDMI_INFOFRAME_TYPE_AVI:
@@ -1516,6 +1522,29 @@ hdmi_vendor_any_infoframe_log(const char *level,
}
}
+static void
+hdmi_emp_infoframe_vtem_log(const char *level,
+ struct device *dev,
+ const struct hdmi_emp_infoframe_vtem *frame)
+{
+ hdmi_infoframe_log_header(level, dev,
+ (const struct hdmi_any_infoframe *)frame);
+
+ hdmi_log(" game vrr enabled: %u\n", frame->game_vrr_en);
+ hdmi_log(" fva factor minus 1: %u\n", frame->fva_factor_m1);
+ hdmi_log(" m_const: %u\n", frame->m_const);
+ hdmi_log(" base vertical front porch: %u\n", frame->base_vfront);
+ hdmi_log(" base refresh rate: %u\n", frame->base_refresh_rate);
+}
+
+static void
+hdmi_emp_infoframe_log(const char *level,
+ struct device *dev,
+ const union hdmi_infoframe *frame)
+{
+ /* Only support VTEM for now */
+ hdmi_emp_infoframe_vtem_log(level, dev, &frame->vtem);
+}
/**
* hdmi_infoframe_log() - log info of HDMI infoframe
* @level: logging level
@@ -1542,6 +1571,9 @@ void hdmi_infoframe_log(const char *level,
case HDMI_INFOFRAME_TYPE_DRM:
hdmi_drm_infoframe_log(level, dev, &frame->drm);
break;
+ case HDMI_INFOFRAME_TYPE_EMP:
+ hdmi_emp_infoframe_log(level, dev, frame);
+ break;
}
}
EXPORT_SYMBOL(hdmi_infoframe_log);
@@ -1919,3 +1951,66 @@ int hdmi_infoframe_unpack(union hdmi_infoframe *frame,
return ret;
}
EXPORT_SYMBOL(hdmi_infoframe_unpack);
+
+/**
+ * hdmi_emp_infoframe_vtem_init() - initialize an HDMI VTEM EMP
+ * @emp: HDMI VTEM EMP
+ */
+void hdmi_emp_infoframe_vtem_init(struct hdmi_emp_infoframe_vtem *emp)
+{
+ memset(emp, 0, sizeof(*emp));
+ emp->type = HDMI_INFOFRAME_TYPE_EMP;
+}
+EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_init);
+
+/**
+ * hdmi_emp_infoframe_vtem_pack() - pack a vtem into a binary buffer
+ * @vtem: pointer to &struct hdmi_emp_infoframe_vtem to pack
+ * @buffer: pointer to pre-allocated output buffer
+ * @size: maximum size in bytes of @buffer
+ *
+ * Pack a VTEM infoframe struct into a binary buffer in a way that's suitable
+ * to be sent out over the wire.
+ *
+ * Returns the number of bytes packed into the binary buffer or a negative
+ * error code on failure.
+ */
+ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
+ void *buffer, size_t size)
+{
+ u8 *ptr = buffer;
+
+ if (size < HDMI_INFOFRAME_SIZE(VTEM))
+ return -EINVAL;
+
+ /* 4 bits max */
+ if (vtem->fva_factor_m1 > 0xF)
+ return -EINVAL;
+
+ memset(buffer, 0, size);
+
+ /* Header Data */
+ ptr[0] = vtem->type;
+ ptr[1] = BIT(7) | BIT(6);
+ ptr[2] = 0;
+
+ ptr[3] = BIT(7) | BIT(2);
+
+ ptr[4] = 0;
+ ptr[5] = 1;
+ ptr[6] = 0;
+ ptr[7] = 1;
+ ptr[8] = 0;
+ ptr[9] = 4;
+
+ /* This is where the VTEM part starts */
+ ptr[10] = (vtem->fva_factor_m1 << 4) |
+ (vtem->m_const ? BIT(1) : 0) |
+ (vtem->game_vrr_en ? BIT(0) : 0);
+ ptr[11] = vtem->base_vfront;
+ ptr[12] = vtem->base_refresh_rate >> 8 & 0x3;
+ ptr[13] = vtem->base_refresh_rate & 0xFF;
+
+ return 14;
+}
+EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_pack);
diff --git a/include/linux/hdmi.h b/include/linux/hdmi.h
index b80a5ee63bb2..71e2b36896af 100644
--- a/include/linux/hdmi.h
+++ b/include/linux/hdmi.h
@@ -55,6 +55,7 @@ enum hdmi_packet_type {
};
enum hdmi_infoframe_type {
+ HDMI_INFOFRAME_TYPE_EMP = 0x7F,
HDMI_INFOFRAME_TYPE_VENDOR = 0x81,
HDMI_INFOFRAME_TYPE_AVI = 0x82,
HDMI_INFOFRAME_TYPE_SPD = 0x83,
@@ -76,6 +77,7 @@ enum hdmi_infoframe_type {
#define HDMI_AUDIO_INFOFRAME_SIZE 10
#define HDMI_DRM_INFOFRAME_SIZE 26
#define HDMI_VENDOR_INFOFRAME_SIZE 4
+#define HDMI_VTEM_INFOFRAME_SIZE 10
/*
* HDMI 1.3a table 5-14 states that the largest InfoFrame_length is 27,
@@ -235,6 +237,18 @@ struct hdmi_drm_infoframe {
u16 max_fall;
};
+struct hdmi_emp_infoframe_vtem {
+ enum hdmi_infoframe_type type;
+ unsigned char version;
+ unsigned char length;
+
+ bool game_vrr_en;
+ bool m_const;
+ unsigned char fva_factor_m1;
+ unsigned char base_vfront;
+ u16 base_refresh_rate;
+};
+
void hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame);
ssize_t hdmi_avi_infoframe_pack(struct hdmi_avi_infoframe *frame, void *buffer,
size_t size);
@@ -249,6 +263,10 @@ ssize_t hdmi_drm_infoframe_pack_only(const struct hdmi_drm_infoframe *frame,
int hdmi_drm_infoframe_check(struct hdmi_drm_infoframe *frame);
int hdmi_drm_infoframe_unpack_only(struct hdmi_drm_infoframe *frame,
const void *buffer, size_t size);
+void hdmi_emp_infoframe_vtem_init(struct hdmi_emp_infoframe_vtem *frame);
+ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *frame,
+ void *buffer,
+ size_t size);
enum hdmi_spd_sdi {
HDMI_SPD_SDI_UNKNOWN,
@@ -457,6 +475,7 @@ union hdmi_infoframe {
union hdmi_vendor_any_infoframe vendor;
struct hdmi_audio_infoframe audio;
struct hdmi_drm_infoframe drm;
+ struct hdmi_emp_infoframe_vtem vtem;
};
ssize_t hdmi_infoframe_pack(union hdmi_infoframe *frame, void *buffer,
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 03/27] video/hdmi: Add VTEM EMP packing
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
VTEM EMPs are a type of infoframe that is required for Variable Refresh
Rate. Add code to pack them.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/video/hdmi.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++-
include/linux/hdmi.h | 19 ++++++++++
2 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/drivers/video/hdmi.c b/drivers/video/hdmi.c
index 45b42f14a750..ed9abc7ad228 100644
--- a/drivers/video/hdmi.c
+++ b/drivers/video/hdmi.c
@@ -983,6 +983,10 @@ hdmi_infoframe_pack(union hdmi_infoframe *frame,
length = hdmi_vendor_any_infoframe_pack(&frame->vendor,
buffer, size);
break;
+ case HDMI_INFOFRAME_TYPE_EMP:
+ length = hdmi_emp_infoframe_vtem_pack(&frame->vtem,
+ buffer, size);
+ break;
default:
WARN(1, "Bad infoframe type %d\n", frame->any.type);
length = -EINVAL;
@@ -994,9 +998,11 @@ EXPORT_SYMBOL(hdmi_infoframe_pack);
static const char *hdmi_infoframe_type_get_name(enum hdmi_infoframe_type type)
{
- if (type < 0x80 || type > 0x9f)
+ if (type < 0x7F || type > 0x9f)
return "Invalid";
switch (type) {
+ case HDMI_INFOFRAME_TYPE_EMP:
+ return "EMP";
case HDMI_INFOFRAME_TYPE_VENDOR:
return "Vendor";
case HDMI_INFOFRAME_TYPE_AVI:
@@ -1516,6 +1522,29 @@ hdmi_vendor_any_infoframe_log(const char *level,
}
}
+static void
+hdmi_emp_infoframe_vtem_log(const char *level,
+ struct device *dev,
+ const struct hdmi_emp_infoframe_vtem *frame)
+{
+ hdmi_infoframe_log_header(level, dev,
+ (const struct hdmi_any_infoframe *)frame);
+
+ hdmi_log(" game vrr enabled: %u\n", frame->game_vrr_en);
+ hdmi_log(" fva factor minus 1: %u\n", frame->fva_factor_m1);
+ hdmi_log(" m_const: %u\n", frame->m_const);
+ hdmi_log(" base vertical front porch: %u\n", frame->base_vfront);
+ hdmi_log(" base refresh rate: %u\n", frame->base_refresh_rate);
+}
+
+static void
+hdmi_emp_infoframe_log(const char *level,
+ struct device *dev,
+ const union hdmi_infoframe *frame)
+{
+ /* Only support VTEM for now */
+ hdmi_emp_infoframe_vtem_log(level, dev, &frame->vtem);
+}
/**
* hdmi_infoframe_log() - log info of HDMI infoframe
* @level: logging level
@@ -1542,6 +1571,9 @@ void hdmi_infoframe_log(const char *level,
case HDMI_INFOFRAME_TYPE_DRM:
hdmi_drm_infoframe_log(level, dev, &frame->drm);
break;
+ case HDMI_INFOFRAME_TYPE_EMP:
+ hdmi_emp_infoframe_log(level, dev, frame);
+ break;
}
}
EXPORT_SYMBOL(hdmi_infoframe_log);
@@ -1919,3 +1951,66 @@ int hdmi_infoframe_unpack(union hdmi_infoframe *frame,
return ret;
}
EXPORT_SYMBOL(hdmi_infoframe_unpack);
+
+/**
+ * hdmi_emp_infoframe_vtem_init() - initialize an HDMI VTEM EMP
+ * @emp: HDMI VTEM EMP
+ */
+void hdmi_emp_infoframe_vtem_init(struct hdmi_emp_infoframe_vtem *emp)
+{
+ memset(emp, 0, sizeof(*emp));
+ emp->type = HDMI_INFOFRAME_TYPE_EMP;
+}
+EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_init);
+
+/**
+ * hdmi_emp_infoframe_vtem_pack() - pack a vtem into a binary buffer
+ * @vtem: pointer to &struct hdmi_emp_infoframe_vtem to pack
+ * @buffer: pointer to pre-allocated output buffer
+ * @size: maximum size in bytes of @buffer
+ *
+ * Pack a VTEM infoframe struct into a binary buffer in a way that's suitable
+ * to be sent out over the wire.
+ *
+ * Returns the number of bytes packed into the binary buffer or a negative
+ * error code on failure.
+ */
+ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
+ void *buffer, size_t size)
+{
+ u8 *ptr = buffer;
+
+ if (size < HDMI_INFOFRAME_SIZE(VTEM))
+ return -EINVAL;
+
+ /* 4 bits max */
+ if (vtem->fva_factor_m1 > 0xF)
+ return -EINVAL;
+
+ memset(buffer, 0, size);
+
+ /* Header Data */
+ ptr[0] = vtem->type;
+ ptr[1] = BIT(7) | BIT(6);
+ ptr[2] = 0;
+
+ ptr[3] = BIT(7) | BIT(2);
+
+ ptr[4] = 0;
+ ptr[5] = 1;
+ ptr[6] = 0;
+ ptr[7] = 1;
+ ptr[8] = 0;
+ ptr[9] = 4;
+
+ /* This is where the VTEM part starts */
+ ptr[10] = (vtem->fva_factor_m1 << 4) |
+ (vtem->m_const ? BIT(1) : 0) |
+ (vtem->game_vrr_en ? BIT(0) : 0);
+ ptr[11] = vtem->base_vfront;
+ ptr[12] = vtem->base_refresh_rate >> 8 & 0x3;
+ ptr[13] = vtem->base_refresh_rate & 0xFF;
+
+ return 14;
+}
+EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_pack);
diff --git a/include/linux/hdmi.h b/include/linux/hdmi.h
index b80a5ee63bb2..71e2b36896af 100644
--- a/include/linux/hdmi.h
+++ b/include/linux/hdmi.h
@@ -55,6 +55,7 @@ enum hdmi_packet_type {
};
enum hdmi_infoframe_type {
+ HDMI_INFOFRAME_TYPE_EMP = 0x7F,
HDMI_INFOFRAME_TYPE_VENDOR = 0x81,
HDMI_INFOFRAME_TYPE_AVI = 0x82,
HDMI_INFOFRAME_TYPE_SPD = 0x83,
@@ -76,6 +77,7 @@ enum hdmi_infoframe_type {
#define HDMI_AUDIO_INFOFRAME_SIZE 10
#define HDMI_DRM_INFOFRAME_SIZE 26
#define HDMI_VENDOR_INFOFRAME_SIZE 4
+#define HDMI_VTEM_INFOFRAME_SIZE 10
/*
* HDMI 1.3a table 5-14 states that the largest InfoFrame_length is 27,
@@ -235,6 +237,18 @@ struct hdmi_drm_infoframe {
u16 max_fall;
};
+struct hdmi_emp_infoframe_vtem {
+ enum hdmi_infoframe_type type;
+ unsigned char version;
+ unsigned char length;
+
+ bool game_vrr_en;
+ bool m_const;
+ unsigned char fva_factor_m1;
+ unsigned char base_vfront;
+ u16 base_refresh_rate;
+};
+
void hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame);
ssize_t hdmi_avi_infoframe_pack(struct hdmi_avi_infoframe *frame, void *buffer,
size_t size);
@@ -249,6 +263,10 @@ ssize_t hdmi_drm_infoframe_pack_only(const struct hdmi_drm_infoframe *frame,
int hdmi_drm_infoframe_check(struct hdmi_drm_infoframe *frame);
int hdmi_drm_infoframe_unpack_only(struct hdmi_drm_infoframe *frame,
const void *buffer, size_t size);
+void hdmi_emp_infoframe_vtem_init(struct hdmi_emp_infoframe_vtem *frame);
+ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *frame,
+ void *buffer,
+ size_t size);
enum hdmi_spd_sdi {
HDMI_SPD_SDI_UNKNOWN,
@@ -457,6 +475,7 @@ union hdmi_infoframe {
union hdmi_vendor_any_infoframe vendor;
struct hdmi_audio_infoframe audio;
struct hdmi_drm_infoframe drm;
+ struct hdmi_emp_infoframe_vtem vtem;
};
ssize_t hdmi_infoframe_pack(union hdmi_infoframe *frame, void *buffer,
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 04/27] drm/bridge: Add VTEM EMP support
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add setters and support bit for VTEM emp to the bridge.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
---
drivers/gpu/drm/display/drm_bridge_connector.c | 41 ++++++++++++++++++++++++++
include/drm/drm_bridge.h | 29 ++++++++++++++++++
include/drm/drm_connector.h | 8 +++++
3 files changed, 78 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c
index 2202e6044444..0fbdba584c1b 100644
--- a/drivers/gpu/drm/display/drm_bridge_connector.c
+++ b/drivers/gpu/drm/display/drm_bridge_connector.c
@@ -611,6 +611,33 @@ static int drm_bridge_connector_scrambler_disable(struct drm_connector *connecto
return bridge->funcs->hdmi_scrambler_disable(bridge);
}
+static int drm_bridge_connector_clear_vtem_emp(struct drm_connector *connector)
+{
+ struct drm_bridge_connector *bridge_connector =
+ to_drm_bridge_connector(connector);
+ struct drm_bridge *bridge;
+
+ bridge = bridge_connector->bridge_hdmi;
+ if (!bridge)
+ return -EINVAL;
+
+ return bridge->funcs->hdmi_clear_vtem_emp(bridge);
+}
+
+static int drm_bridge_connector_write_vtem_emp(struct drm_connector *connector,
+ const u8 *buffer, size_t len)
+{
+ struct drm_bridge_connector *bridge_connector =
+ to_drm_bridge_connector(connector);
+ struct drm_bridge *bridge;
+
+ bridge = bridge_connector->bridge_hdmi;
+ if (!bridge)
+ return -EINVAL;
+
+ return bridge->funcs->hdmi_write_vtem_emp(bridge, buffer, len);
+}
+
static const struct drm_edid *
drm_bridge_connector_read_edid(struct drm_connector *connector)
{
@@ -656,6 +683,11 @@ static const struct drm_connector_infoframe_funcs drm_bridge_connector_hdmi_spd_
.write_infoframe = drm_bridge_connector_write_spd_infoframe,
};
+static const struct drm_connector_infoframe_funcs drm_bridge_connector_hdmi_vtem_emp = {
+ .clear_infoframe = drm_bridge_connector_clear_vtem_emp,
+ .write_infoframe = drm_bridge_connector_write_vtem_emp,
+};
+
static int drm_bridge_connector_audio_startup(struct drm_connector *connector)
{
struct drm_bridge_connector *bridge_connector =
@@ -947,6 +979,11 @@ struct drm_connector *drm_bridge_connector_init(struct drm_device *drm,
!bridge->funcs->hdmi_scrambler_disable))
return ERR_PTR(-EINVAL);
+ if (bridge->ops & DRM_BRIDGE_OP_HDMI_VTEM_EMP &&
+ (!bridge->funcs->hdmi_write_vtem_emp ||
+ !bridge->funcs->hdmi_clear_vtem_emp))
+ return ERR_PTR(-EINVAL);
+
bridge_connector->bridge_hdmi = drm_bridge_get(bridge);
}
@@ -1073,6 +1110,10 @@ struct drm_connector *drm_bridge_connector_init(struct drm_device *drm,
drm_bridge_connector_scrambler_disable;
}
+ if (bridge_connector->bridge_hdmi->ops & DRM_BRIDGE_OP_HDMI_VTEM_EMP)
+ bridge_connector->hdmi_funcs.vtem =
+ drm_bridge_connector_hdmi_vtem_emp;
+
ret = drmm_connector_hdmi_init(drm, connector,
&drm_bridge_connector_funcs,
&bridge_connector->hdmi_funcs,
diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h
index 1981d24a700d..6913662023ca 100644
--- a/include/drm/drm_bridge.h
+++ b/include/drm/drm_bridge.h
@@ -684,6 +684,29 @@ struct drm_bridge_funcs {
int (*hdmi_write_spd_infoframe)(struct drm_bridge *bridge,
const u8 *buffer, size_t len);
+ /**
+ * @hdmi_clear_vtem_emp:
+ *
+ * This callback clears the emp in the hardware during commit.
+ *
+ * This callback is optional but it must be implemented by bridges that
+ * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
+ * &drm_bridge->ops.
+ */
+ int (*hdmi_clear_vtem_emp)(struct drm_bridge *bridge);
+
+ /**
+ * @hdmi_write_spd_infoframe:
+ *
+ * Program the emp into the hardware.
+ *
+ * This callback is optional but it must be implemented by bridges that
+ * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
+ * &drm_bridge->ops.
+ */
+ int (*hdmi_write_vtem_emp)(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len);
+
/**
* @hdmi_clear_audio_infoframe:
*
@@ -1021,6 +1044,12 @@ enum drm_bridge_ops {
* &drm_bridge_funcs->hdmi_clear_spd_infoframe callbacks.
*/
DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME = BIT(10),
+ /**
+ * @DRM_BRIDGE_OP_HDMI_VTEM_EMP: The bridge supports
+ * &drm_bridge_funcs->hdmi_write_vtem_emp and
+ * &drm_bridge_funcs->hdmi_clear_vtem_emp callbacks.
+ */
+ DRM_BRIDGE_OP_HDMI_VTEM_EMP = BIT(11),
};
/**
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 8699f35cafcb..184b10c89508 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1645,6 +1645,14 @@ struct drm_connector_hdmi_funcs {
* optional.
*/
struct drm_connector_infoframe_funcs spd;
+
+ /**
+ * @vtem:
+ *
+ * Set of callbacks for handling the VTEM EMP. These callbacks are
+ * optional.
+ */
+ struct drm_connector_infoframe_funcs vtem;
};
/**
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 04/27] drm/bridge: Add VTEM EMP support
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add setters and support bit for VTEM emp to the bridge.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
---
drivers/gpu/drm/display/drm_bridge_connector.c | 41 ++++++++++++++++++++++++++
include/drm/drm_bridge.h | 29 ++++++++++++++++++
include/drm/drm_connector.h | 8 +++++
3 files changed, 78 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c
index 2202e6044444..0fbdba584c1b 100644
--- a/drivers/gpu/drm/display/drm_bridge_connector.c
+++ b/drivers/gpu/drm/display/drm_bridge_connector.c
@@ -611,6 +611,33 @@ static int drm_bridge_connector_scrambler_disable(struct drm_connector *connecto
return bridge->funcs->hdmi_scrambler_disable(bridge);
}
+static int drm_bridge_connector_clear_vtem_emp(struct drm_connector *connector)
+{
+ struct drm_bridge_connector *bridge_connector =
+ to_drm_bridge_connector(connector);
+ struct drm_bridge *bridge;
+
+ bridge = bridge_connector->bridge_hdmi;
+ if (!bridge)
+ return -EINVAL;
+
+ return bridge->funcs->hdmi_clear_vtem_emp(bridge);
+}
+
+static int drm_bridge_connector_write_vtem_emp(struct drm_connector *connector,
+ const u8 *buffer, size_t len)
+{
+ struct drm_bridge_connector *bridge_connector =
+ to_drm_bridge_connector(connector);
+ struct drm_bridge *bridge;
+
+ bridge = bridge_connector->bridge_hdmi;
+ if (!bridge)
+ return -EINVAL;
+
+ return bridge->funcs->hdmi_write_vtem_emp(bridge, buffer, len);
+}
+
static const struct drm_edid *
drm_bridge_connector_read_edid(struct drm_connector *connector)
{
@@ -656,6 +683,11 @@ static const struct drm_connector_infoframe_funcs drm_bridge_connector_hdmi_spd_
.write_infoframe = drm_bridge_connector_write_spd_infoframe,
};
+static const struct drm_connector_infoframe_funcs drm_bridge_connector_hdmi_vtem_emp = {
+ .clear_infoframe = drm_bridge_connector_clear_vtem_emp,
+ .write_infoframe = drm_bridge_connector_write_vtem_emp,
+};
+
static int drm_bridge_connector_audio_startup(struct drm_connector *connector)
{
struct drm_bridge_connector *bridge_connector =
@@ -947,6 +979,11 @@ struct drm_connector *drm_bridge_connector_init(struct drm_device *drm,
!bridge->funcs->hdmi_scrambler_disable))
return ERR_PTR(-EINVAL);
+ if (bridge->ops & DRM_BRIDGE_OP_HDMI_VTEM_EMP &&
+ (!bridge->funcs->hdmi_write_vtem_emp ||
+ !bridge->funcs->hdmi_clear_vtem_emp))
+ return ERR_PTR(-EINVAL);
+
bridge_connector->bridge_hdmi = drm_bridge_get(bridge);
}
@@ -1073,6 +1110,10 @@ struct drm_connector *drm_bridge_connector_init(struct drm_device *drm,
drm_bridge_connector_scrambler_disable;
}
+ if (bridge_connector->bridge_hdmi->ops & DRM_BRIDGE_OP_HDMI_VTEM_EMP)
+ bridge_connector->hdmi_funcs.vtem =
+ drm_bridge_connector_hdmi_vtem_emp;
+
ret = drmm_connector_hdmi_init(drm, connector,
&drm_bridge_connector_funcs,
&bridge_connector->hdmi_funcs,
diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h
index 1981d24a700d..6913662023ca 100644
--- a/include/drm/drm_bridge.h
+++ b/include/drm/drm_bridge.h
@@ -684,6 +684,29 @@ struct drm_bridge_funcs {
int (*hdmi_write_spd_infoframe)(struct drm_bridge *bridge,
const u8 *buffer, size_t len);
+ /**
+ * @hdmi_clear_vtem_emp:
+ *
+ * This callback clears the emp in the hardware during commit.
+ *
+ * This callback is optional but it must be implemented by bridges that
+ * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
+ * &drm_bridge->ops.
+ */
+ int (*hdmi_clear_vtem_emp)(struct drm_bridge *bridge);
+
+ /**
+ * @hdmi_write_spd_infoframe:
+ *
+ * Program the emp into the hardware.
+ *
+ * This callback is optional but it must be implemented by bridges that
+ * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
+ * &drm_bridge->ops.
+ */
+ int (*hdmi_write_vtem_emp)(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len);
+
/**
* @hdmi_clear_audio_infoframe:
*
@@ -1021,6 +1044,12 @@ enum drm_bridge_ops {
* &drm_bridge_funcs->hdmi_clear_spd_infoframe callbacks.
*/
DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME = BIT(10),
+ /**
+ * @DRM_BRIDGE_OP_HDMI_VTEM_EMP: The bridge supports
+ * &drm_bridge_funcs->hdmi_write_vtem_emp and
+ * &drm_bridge_funcs->hdmi_clear_vtem_emp callbacks.
+ */
+ DRM_BRIDGE_OP_HDMI_VTEM_EMP = BIT(11),
};
/**
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 8699f35cafcb..184b10c89508 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1645,6 +1645,14 @@ struct drm_connector_hdmi_funcs {
* optional.
*/
struct drm_connector_infoframe_funcs spd;
+
+ /**
+ * @vtem:
+ *
+ * Set of callbacks for handling the VTEM EMP. These callbacks are
+ * optional.
+ */
+ struct drm_connector_infoframe_funcs vtem;
};
/**
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 05/27] drm/connector: hdmi: Add VTEM EMP generation
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add VTEM EMP generation to enable variable refresh rate signalling over
HDMI.
These infoframes are only generated if the sink supports VRR.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 63 +++++++++++++++++++++++++
include/drm/drm_connector.h | 5 ++
2 files changed, 68 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index eb6abfd1350d..ce680eaa5047 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -853,6 +853,61 @@ static int hdmi_generate_hdmi_vendor_infoframe(const struct drm_connector *conne
return 0;
}
+static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connector,
+ struct drm_connector_state *conn_state)
+{
+ const struct drm_display_info *info = &connector->display_info;
+ const struct drm_crtc_state *crtc_state =
+ drm_atomic_get_crtc_state(conn_state->state, conn_state->crtc);
+ struct drm_connector_hdmi_infoframe *infoframe =
+ &conn_state->hdmi.infoframes.vtem;
+ struct hdmi_emp_infoframe_vtem *vtem =
+ &infoframe->data.vtem;
+ const struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+ int vfront;
+ u8 avi_vic;
+
+ infoframe->set = false;
+
+ if (!connector->hdmi.funcs->vtem.write_infoframe)
+ return 0;
+
+ if (!info->hdmi.vrr_cap.supported)
+ return 0;
+
+ if (conn_state->hdmi.infoframes.avi.set)
+ avi_vic = conn_state->hdmi.infoframes.avi.data.avi.video_code;
+ else
+ avi_vic = 0;
+
+ hdmi_emp_infoframe_vtem_init(vtem);
+
+ switch (conn_state->vrr_display_mode) {
+ case VRR_DISPLAY_MODE_GAME:
+ vtem->game_vrr_en = true;
+ vtem->m_const = !vrr->dynamic;
+ break;
+ case VRR_DISPLAY_MODE_OFF:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (vtem->game_vrr_en && !avi_vic) {
+ vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
+ vfront = crtc_state->adjusted_mode.crtc_vsync_start -
+ crtc_state->adjusted_mode.crtc_vdisplay;
+ if (vfront > U8_MAX || vfront < 0)
+ return -EINVAL;
+
+ vtem->base_vfront = vfront;
+ }
+
+ infoframe->set = true;
+
+ return 0;
+}
+
static int
hdmi_generate_infoframes(const struct drm_connector *connector,
struct drm_connector_state *conn_state)
@@ -884,6 +939,10 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
if (ret)
return ret;
+ ret = hdmi_generate_emp_infoframe_vtem(connector, conn_state);
+ if (ret)
+ return ret;
+
return 0;
}
@@ -1475,6 +1534,10 @@ int drm_atomic_helper_connector_hdmi_update_infoframes(struct drm_connector *con
goto out;
}
+ ret = write_or_clear_infoframe(connector, &funcs->vtem, "VTEM",
+ &old_conn_state->hdmi.infoframes.vtem,
+ &new_conn_state->hdmi.infoframes.vtem);
+
out:
mutex_unlock(&connector->hdmi.infoframes.lock);
return ret;
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 184b10c89508..9df745ecff92 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1186,6 +1186,11 @@ struct drm_connector_hdmi_state {
* matching our state.
*/
struct drm_connector_hdmi_infoframe hdmi;
+
+ /**
+ * @vtem: VTEM EMP infoframes structure matching our state.
+ */
+ struct drm_connector_hdmi_infoframe vtem;
} infoframes;
/**
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 05/27] drm/connector: hdmi: Add VTEM EMP generation
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add VTEM EMP generation to enable variable refresh rate signalling over
HDMI.
These infoframes are only generated if the sink supports VRR.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 63 +++++++++++++++++++++++++
include/drm/drm_connector.h | 5 ++
2 files changed, 68 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index eb6abfd1350d..ce680eaa5047 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -853,6 +853,61 @@ static int hdmi_generate_hdmi_vendor_infoframe(const struct drm_connector *conne
return 0;
}
+static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connector,
+ struct drm_connector_state *conn_state)
+{
+ const struct drm_display_info *info = &connector->display_info;
+ const struct drm_crtc_state *crtc_state =
+ drm_atomic_get_crtc_state(conn_state->state, conn_state->crtc);
+ struct drm_connector_hdmi_infoframe *infoframe =
+ &conn_state->hdmi.infoframes.vtem;
+ struct hdmi_emp_infoframe_vtem *vtem =
+ &infoframe->data.vtem;
+ const struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+ int vfront;
+ u8 avi_vic;
+
+ infoframe->set = false;
+
+ if (!connector->hdmi.funcs->vtem.write_infoframe)
+ return 0;
+
+ if (!info->hdmi.vrr_cap.supported)
+ return 0;
+
+ if (conn_state->hdmi.infoframes.avi.set)
+ avi_vic = conn_state->hdmi.infoframes.avi.data.avi.video_code;
+ else
+ avi_vic = 0;
+
+ hdmi_emp_infoframe_vtem_init(vtem);
+
+ switch (conn_state->vrr_display_mode) {
+ case VRR_DISPLAY_MODE_GAME:
+ vtem->game_vrr_en = true;
+ vtem->m_const = !vrr->dynamic;
+ break;
+ case VRR_DISPLAY_MODE_OFF:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (vtem->game_vrr_en && !avi_vic) {
+ vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
+ vfront = crtc_state->adjusted_mode.crtc_vsync_start -
+ crtc_state->adjusted_mode.crtc_vdisplay;
+ if (vfront > U8_MAX || vfront < 0)
+ return -EINVAL;
+
+ vtem->base_vfront = vfront;
+ }
+
+ infoframe->set = true;
+
+ return 0;
+}
+
static int
hdmi_generate_infoframes(const struct drm_connector *connector,
struct drm_connector_state *conn_state)
@@ -884,6 +939,10 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
if (ret)
return ret;
+ ret = hdmi_generate_emp_infoframe_vtem(connector, conn_state);
+ if (ret)
+ return ret;
+
return 0;
}
@@ -1475,6 +1534,10 @@ int drm_atomic_helper_connector_hdmi_update_infoframes(struct drm_connector *con
goto out;
}
+ ret = write_or_clear_infoframe(connector, &funcs->vtem, "VTEM",
+ &old_conn_state->hdmi.infoframes.vtem,
+ &new_conn_state->hdmi.infoframes.vtem);
+
out:
mutex_unlock(&connector->hdmi.infoframes.lock);
return ret;
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 184b10c89508..9df745ecff92 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1186,6 +1186,11 @@ struct drm_connector_hdmi_state {
* matching our state.
*/
struct drm_connector_hdmi_infoframe hdmi;
+
+ /**
+ * @vtem: VTEM EMP infoframes structure matching our state.
+ */
+ struct drm_connector_hdmi_infoframe vtem;
} infoframes;
/**
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 06/27] drm/crtc-helper: Add VRR helper functions
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
VRR needs to change the vertical front porch timing to change the duration
of a frame. Add two helpers to aid in this.
drm_crtc_helper_vrr_flush should be called when the source has a new
frame ready to present. It adjusts the internal VRR state, and returns
whether the current vtotal has been changed. Callers can then adjust
their hardware state as is necessary.
drm_crtc_helper_vrr_vsync should be called in the vblank interrupt
handler. It adjusts the internal VRR state, and returns whether the
current vtotal has been changed. Callers can then adjust their hardware
state as is necessary.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_crtc_helper.c | 63 +++++++++++++++++++++++++++++++++++++++
include/drm/drm_crtc_helper.h | 4 +++
2 files changed, 67 insertions(+)
diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
index 4a8cfe046e4a..4ffb3b0b7878 100644
--- a/drivers/gpu/drm/drm_crtc_helper.c
+++ b/drivers/gpu/drm/drm_crtc_helper.c
@@ -838,6 +838,69 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
}
EXPORT_SYMBOL(drm_crtc_helper_set_config);
+/**
+ * drm_crtc_helper_vrr_flush - Update VRR state on frame ready
+ * @crtc_state: pointer to &struct drm_crtc_state of CRTC that has a new frame ready
+ *
+ * Update the VRR state by communicating that a frame is ready on
+ * @crtc_state.crtc.
+ *
+ * Should be called from the @crtc_state.crtc &drm_crtc_helper_funcs.atomic_flush
+ * hook.
+ *
+ * Returns:
+ * - %true if @crtc_state.vrr_state.cur_vtotal changed
+ * - %false otherwise
+ */
+bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state)
+{
+ struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+
+ if (!crtc_state->vrr_enabled) {
+ if (vrr->cur_vtotal) {
+ vrr->cur_vtotal = 0;
+ return true;
+ }
+ return false;
+ }
+
+ if (vrr->cur_vtotal == vrr->base_vtotal)
+ return false;
+
+ vrr->cur_vtotal = vrr->base_vtotal;
+
+ return true;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_flush);
+
+/**
+ * drm_crtc_helper_vrr_vsync - Update VRR state on vblank end
+ * @crtc_state: pointer to &struct drm_crtc_state of CRTC the vblank occurred on
+ *
+ * Update the VRR state by communicating that a vertical blank has ended and a
+ * new frame is about to start.
+ *
+ * Returns:
+ * - %true if @crtc_state.vrr_state.cur_vtotal changed
+ * - %false otherwise
+ */
+bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
+{
+ struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+ u16 old_vtotal = vrr->cur_vtotal;
+
+ if (!crtc_state->vrr_enabled)
+ vrr->cur_vtotal = 0;
+ else
+ vrr->cur_vtotal = vrr->max_vtotal;
+
+ if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
+ vrr->dynamic = true;
+
+ return old_vtotal != vrr->cur_vtotal;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_vsync);
+
static int drm_helper_choose_encoder_dpms(struct drm_encoder *encoder)
{
int dpms = DRM_MODE_DPMS_OFF;
diff --git a/include/drm/drm_crtc_helper.h b/include/drm/drm_crtc_helper.h
index 855da5733c47..0c8c11ea36dd 100644
--- a/include/drm/drm_crtc_helper.h
+++ b/include/drm/drm_crtc_helper.h
@@ -44,6 +44,7 @@ struct drm_encoder;
struct drm_framebuffer;
struct drm_mode_set;
struct drm_modeset_acquire_ctx;
+struct drm_crtc_state;
void drm_helper_disable_unused_functions(struct drm_device *dev);
int drm_crtc_helper_set_config(struct drm_mode_set *set,
@@ -62,4 +63,7 @@ int drm_helper_connector_dpms(struct drm_connector *connector, int mode);
void drm_helper_resume_force_mode(struct drm_device *dev);
int drm_helper_force_disable_all(struct drm_device *dev);
+bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state);
+bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state);
+
#endif
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 06/27] drm/crtc-helper: Add VRR helper functions
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
VRR needs to change the vertical front porch timing to change the duration
of a frame. Add two helpers to aid in this.
drm_crtc_helper_vrr_flush should be called when the source has a new
frame ready to present. It adjusts the internal VRR state, and returns
whether the current vtotal has been changed. Callers can then adjust
their hardware state as is necessary.
drm_crtc_helper_vrr_vsync should be called in the vblank interrupt
handler. It adjusts the internal VRR state, and returns whether the
current vtotal has been changed. Callers can then adjust their hardware
state as is necessary.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_crtc_helper.c | 63 +++++++++++++++++++++++++++++++++++++++
include/drm/drm_crtc_helper.h | 4 +++
2 files changed, 67 insertions(+)
diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
index 4a8cfe046e4a..4ffb3b0b7878 100644
--- a/drivers/gpu/drm/drm_crtc_helper.c
+++ b/drivers/gpu/drm/drm_crtc_helper.c
@@ -838,6 +838,69 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
}
EXPORT_SYMBOL(drm_crtc_helper_set_config);
+/**
+ * drm_crtc_helper_vrr_flush - Update VRR state on frame ready
+ * @crtc_state: pointer to &struct drm_crtc_state of CRTC that has a new frame ready
+ *
+ * Update the VRR state by communicating that a frame is ready on
+ * @crtc_state.crtc.
+ *
+ * Should be called from the @crtc_state.crtc &drm_crtc_helper_funcs.atomic_flush
+ * hook.
+ *
+ * Returns:
+ * - %true if @crtc_state.vrr_state.cur_vtotal changed
+ * - %false otherwise
+ */
+bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state)
+{
+ struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+
+ if (!crtc_state->vrr_enabled) {
+ if (vrr->cur_vtotal) {
+ vrr->cur_vtotal = 0;
+ return true;
+ }
+ return false;
+ }
+
+ if (vrr->cur_vtotal == vrr->base_vtotal)
+ return false;
+
+ vrr->cur_vtotal = vrr->base_vtotal;
+
+ return true;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_flush);
+
+/**
+ * drm_crtc_helper_vrr_vsync - Update VRR state on vblank end
+ * @crtc_state: pointer to &struct drm_crtc_state of CRTC the vblank occurred on
+ *
+ * Update the VRR state by communicating that a vertical blank has ended and a
+ * new frame is about to start.
+ *
+ * Returns:
+ * - %true if @crtc_state.vrr_state.cur_vtotal changed
+ * - %false otherwise
+ */
+bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
+{
+ struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
+ u16 old_vtotal = vrr->cur_vtotal;
+
+ if (!crtc_state->vrr_enabled)
+ vrr->cur_vtotal = 0;
+ else
+ vrr->cur_vtotal = vrr->max_vtotal;
+
+ if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
+ vrr->dynamic = true;
+
+ return old_vtotal != vrr->cur_vtotal;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_vsync);
+
static int drm_helper_choose_encoder_dpms(struct drm_encoder *encoder)
{
int dpms = DRM_MODE_DPMS_OFF;
diff --git a/include/drm/drm_crtc_helper.h b/include/drm/drm_crtc_helper.h
index 855da5733c47..0c8c11ea36dd 100644
--- a/include/drm/drm_crtc_helper.h
+++ b/include/drm/drm_crtc_helper.h
@@ -44,6 +44,7 @@ struct drm_encoder;
struct drm_framebuffer;
struct drm_mode_set;
struct drm_modeset_acquire_ctx;
+struct drm_crtc_state;
void drm_helper_disable_unused_functions(struct drm_device *dev);
int drm_crtc_helper_set_config(struct drm_mode_set *set,
@@ -62,4 +63,7 @@ int drm_helper_connector_dpms(struct drm_connector *connector, int mode);
void drm_helper_resume_force_mode(struct drm_device *dev);
int drm_helper_force_disable_all(struct drm_device *dev);
+bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state);
+bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state);
+
#endif
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 07/27] drm/bridge: synopsys: Add VTEM EMP support
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add code to pack and schedule VTEM EMPs.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
---
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 50 +++++++++++++++++++++++++++-
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h | 1 +
2 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
index b268c9376087..8b3f586dc177 100644
--- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
+++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
@@ -689,6 +689,15 @@ static int dw_hdmi_qp_bridge_clear_spd_infoframe(struct drm_bridge *bridge)
return 0;
}
+static int dw_hdmi_qp_bridge_clear_vtem_emp(struct drm_bridge *bridge)
+{
+ struct dw_hdmi_qp *hdmi = bridge->driver_private;
+
+ dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_EMP_VTEM_TX_EN, PKTSCHED_PKT_EN);
+
+ return 0;
+}
+
static int dw_hdmi_qp_bridge_clear_audio_infoframe(struct drm_bridge *bridge)
{
struct dw_hdmi_qp *hdmi = bridge->driver_private;
@@ -791,6 +800,42 @@ static int dw_hdmi_qp_bridge_write_spd_infoframe(struct drm_bridge *bridge,
return 0;
}
+static int dw_hdmi_qp_bridge_write_vtem_emp(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ struct dw_hdmi_qp *hdmi = bridge->driver_private;
+ u32 val, i;
+
+ if (len != HDMI_INFOFRAME_SIZE(VTEM)) {
+ dev_err(hdmi->dev, "unsupported vtem emp packet size of %zu\n", len);
+ return -EINVAL;
+ }
+
+ val = buffer[1] << 8 | buffer[2] << 16;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS0);
+
+ val = buffer[3] | buffer[4] << 8;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS1);
+
+ val = buffer[8] << 8 | buffer[9] << 16 | buffer[10] << 24;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS2);
+
+ val = buffer[11] | buffer[12] << 8 | buffer[13] << 16;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS3);
+
+ /*
+ * The remaining registers need to explicitly be rewritten even if they
+ * stay at 0, as per the hardware documentation.
+ */
+ for (i = PKT_EMP_VTEM_CONTENTS4; i <= PKT_EMP_VTEM_CONTENTS7; i += 4)
+ dw_hdmi_qp_write(hdmi, 0, i);
+
+ dw_hdmi_qp_mod(hdmi, PKTSCHED_EMP_VTEM_TX_EN, PKTSCHED_EMP_VTEM_TX_EN,
+ PKTSCHED_PKT_EN);
+
+ return 0;
+}
+
static int dw_hdmi_qp_bridge_write_audio_infoframe(struct drm_bridge *bridge,
const u8 *buffer, size_t len)
{
@@ -1035,6 +1080,8 @@ static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = {
.hdmi_write_hdr_drm_infoframe = dw_hdmi_qp_bridge_write_hdr_drm_infoframe,
.hdmi_clear_spd_infoframe = dw_hdmi_qp_bridge_clear_spd_infoframe,
.hdmi_write_spd_infoframe = dw_hdmi_qp_bridge_write_spd_infoframe,
+ .hdmi_clear_vtem_emp = dw_hdmi_qp_bridge_clear_vtem_emp,
+ .hdmi_write_vtem_emp = dw_hdmi_qp_bridge_write_vtem_emp,
.hdmi_clear_audio_infoframe = dw_hdmi_qp_bridge_clear_audio_infoframe,
.hdmi_write_audio_infoframe = dw_hdmi_qp_bridge_write_audio_infoframe,
.hdmi_audio_startup = dw_hdmi_qp_audio_enable,
@@ -1154,7 +1201,8 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev,
DRM_BRIDGE_OP_HDMI |
DRM_BRIDGE_OP_HDMI_AUDIO |
DRM_BRIDGE_OP_HDMI_HDR_DRM_INFOFRAME |
- DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME;
+ DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME |
+ DRM_BRIDGE_OP_HDMI_VTEM_EMP;
hdmi->bridge.of_node = pdev->dev.of_node;
hdmi->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
hdmi->bridge.vendor = "Synopsys";
diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
index c07847e8d7dd..6ce87064a267 100644
--- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
+++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
@@ -210,6 +210,7 @@
#define PKTSCHED_AVI_TX_EN BIT(13)
#define PKTSCHED_VSI_TX_EN BIT(12)
#define PKTSCHED_EMP_CVTEM_TX_EN BIT(10)
+#define PKTSCHED_EMP_VTEM_TX_EN BIT(9)
#define PKTSCHED_AMD_TX_EN BIT(8)
#define PKTSCHED_GCP_TX_EN BIT(3)
#define PKTSCHED_AUDS_TX_EN BIT(2)
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 07/27] drm/bridge: synopsys: Add VTEM EMP support
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
From: Derek Foreman <derek.foreman@collabora.com>
Add code to pack and schedule VTEM EMPs.
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
---
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 50 +++++++++++++++++++++++++++-
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h | 1 +
2 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
index b268c9376087..8b3f586dc177 100644
--- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
+++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
@@ -689,6 +689,15 @@ static int dw_hdmi_qp_bridge_clear_spd_infoframe(struct drm_bridge *bridge)
return 0;
}
+static int dw_hdmi_qp_bridge_clear_vtem_emp(struct drm_bridge *bridge)
+{
+ struct dw_hdmi_qp *hdmi = bridge->driver_private;
+
+ dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_EMP_VTEM_TX_EN, PKTSCHED_PKT_EN);
+
+ return 0;
+}
+
static int dw_hdmi_qp_bridge_clear_audio_infoframe(struct drm_bridge *bridge)
{
struct dw_hdmi_qp *hdmi = bridge->driver_private;
@@ -791,6 +800,42 @@ static int dw_hdmi_qp_bridge_write_spd_infoframe(struct drm_bridge *bridge,
return 0;
}
+static int dw_hdmi_qp_bridge_write_vtem_emp(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ struct dw_hdmi_qp *hdmi = bridge->driver_private;
+ u32 val, i;
+
+ if (len != HDMI_INFOFRAME_SIZE(VTEM)) {
+ dev_err(hdmi->dev, "unsupported vtem emp packet size of %zu\n", len);
+ return -EINVAL;
+ }
+
+ val = buffer[1] << 8 | buffer[2] << 16;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS0);
+
+ val = buffer[3] | buffer[4] << 8;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS1);
+
+ val = buffer[8] << 8 | buffer[9] << 16 | buffer[10] << 24;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS2);
+
+ val = buffer[11] | buffer[12] << 8 | buffer[13] << 16;
+ dw_hdmi_qp_write(hdmi, val, PKT_EMP_VTEM_CONTENTS3);
+
+ /*
+ * The remaining registers need to explicitly be rewritten even if they
+ * stay at 0, as per the hardware documentation.
+ */
+ for (i = PKT_EMP_VTEM_CONTENTS4; i <= PKT_EMP_VTEM_CONTENTS7; i += 4)
+ dw_hdmi_qp_write(hdmi, 0, i);
+
+ dw_hdmi_qp_mod(hdmi, PKTSCHED_EMP_VTEM_TX_EN, PKTSCHED_EMP_VTEM_TX_EN,
+ PKTSCHED_PKT_EN);
+
+ return 0;
+}
+
static int dw_hdmi_qp_bridge_write_audio_infoframe(struct drm_bridge *bridge,
const u8 *buffer, size_t len)
{
@@ -1035,6 +1080,8 @@ static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = {
.hdmi_write_hdr_drm_infoframe = dw_hdmi_qp_bridge_write_hdr_drm_infoframe,
.hdmi_clear_spd_infoframe = dw_hdmi_qp_bridge_clear_spd_infoframe,
.hdmi_write_spd_infoframe = dw_hdmi_qp_bridge_write_spd_infoframe,
+ .hdmi_clear_vtem_emp = dw_hdmi_qp_bridge_clear_vtem_emp,
+ .hdmi_write_vtem_emp = dw_hdmi_qp_bridge_write_vtem_emp,
.hdmi_clear_audio_infoframe = dw_hdmi_qp_bridge_clear_audio_infoframe,
.hdmi_write_audio_infoframe = dw_hdmi_qp_bridge_write_audio_infoframe,
.hdmi_audio_startup = dw_hdmi_qp_audio_enable,
@@ -1154,7 +1201,8 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev,
DRM_BRIDGE_OP_HDMI |
DRM_BRIDGE_OP_HDMI_AUDIO |
DRM_BRIDGE_OP_HDMI_HDR_DRM_INFOFRAME |
- DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME;
+ DRM_BRIDGE_OP_HDMI_SPD_INFOFRAME |
+ DRM_BRIDGE_OP_HDMI_VTEM_EMP;
hdmi->bridge.of_node = pdev->dev.of_node;
hdmi->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
hdmi->bridge.vendor = "Synopsys";
diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
index c07847e8d7dd..6ce87064a267 100644
--- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
+++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h
@@ -210,6 +210,7 @@
#define PKTSCHED_AVI_TX_EN BIT(13)
#define PKTSCHED_VSI_TX_EN BIT(12)
#define PKTSCHED_EMP_CVTEM_TX_EN BIT(10)
+#define PKTSCHED_EMP_VTEM_TX_EN BIT(9)
#define PKTSCHED_AMD_TX_EN BIT(8)
#define PKTSCHED_GCP_TX_EN BIT(3)
#define PKTSCHED_AUDS_TX_EN BIT(2)
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 08/27] drm/connector: Add drm_display_info_is_vrr_capable
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:51 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add a function to return whether a sink, as described by a
drm_display_info struct, is capable of receiving VRR signals.
For HDMI sinks, this can just check the HDMI specific vrr_cap.supported
field, which is set by the EDID parsing code. For other types of
displays, the generally agreed-upon method appears to be to check
whether the monitor range's minimum frequency is above 0.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_connector.c | 19 +++++++++++++++++++
include/drm/drm_connector.h | 2 ++
2 files changed, 21 insertions(+)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 65c14b6e47a6..a44afba8c5ff 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1378,6 +1378,25 @@ int drm_display_info_set_bus_formats(struct drm_display_info *info,
}
EXPORT_SYMBOL(drm_display_info_set_bus_formats);
+/**
+ * drm_display_info_is_vrr_capable - Check whether sink is VRR capable
+ * @info: pointer to a &struct drm_display_info representing the sink
+ *
+ * Check whether the sink supports variable refresh rate signals.
+ *
+ * Returns:
+ * - true if variable refresh rate is supported by the sink
+ * - false otherwise
+ */
+bool drm_display_info_is_vrr_capable(const struct drm_display_info *info)
+{
+ if (info->is_hdmi)
+ return info->hdmi.vrr_cap.supported;
+
+ return info->monitor_range.min_vfreq > 0;
+}
+EXPORT_SYMBOL(drm_display_info_is_vrr_capable);
+
/* Optional connector properties. */
static const struct drm_prop_enum_list drm_scaling_mode_enum_list[] = {
{ DRM_MODE_SCALE_NONE, "None" },
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 9df745ecff92..5b6920c1a996 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1078,6 +1078,8 @@ int drm_display_info_set_bus_formats(struct drm_display_info *info,
const u32 *formats,
unsigned int num_formats);
+bool drm_display_info_is_vrr_capable(const struct drm_display_info *info);
+
/**
* struct drm_connector_tv_margins - TV connector related margins
*
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 08/27] drm/connector: Add drm_display_info_is_vrr_capable
@ 2026-11-05 15:51 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:51 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add a function to return whether a sink, as described by a
drm_display_info struct, is capable of receiving VRR signals.
For HDMI sinks, this can just check the HDMI specific vrr_cap.supported
field, which is set by the EDID parsing code. For other types of
displays, the generally agreed-upon method appears to be to check
whether the monitor range's minimum frequency is above 0.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_connector.c | 19 +++++++++++++++++++
include/drm/drm_connector.h | 2 ++
2 files changed, 21 insertions(+)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 65c14b6e47a6..a44afba8c5ff 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1378,6 +1378,25 @@ int drm_display_info_set_bus_formats(struct drm_display_info *info,
}
EXPORT_SYMBOL(drm_display_info_set_bus_formats);
+/**
+ * drm_display_info_is_vrr_capable - Check whether sink is VRR capable
+ * @info: pointer to a &struct drm_display_info representing the sink
+ *
+ * Check whether the sink supports variable refresh rate signals.
+ *
+ * Returns:
+ * - true if variable refresh rate is supported by the sink
+ * - false otherwise
+ */
+bool drm_display_info_is_vrr_capable(const struct drm_display_info *info)
+{
+ if (info->is_hdmi)
+ return info->hdmi.vrr_cap.supported;
+
+ return info->monitor_range.min_vfreq > 0;
+}
+EXPORT_SYMBOL(drm_display_info_is_vrr_capable);
+
/* Optional connector properties. */
static const struct drm_prop_enum_list drm_scaling_mode_enum_list[] = {
{ DRM_MODE_SCALE_NONE, "None" },
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 9df745ecff92..5b6920c1a996 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1078,6 +1078,8 @@ int drm_display_info_set_bus_formats(struct drm_display_info *info,
const u32 *formats,
unsigned int num_formats);
+bool drm_display_info_is_vrr_capable(const struct drm_display_info *info);
+
/**
* struct drm_connector_tv_margins - TV connector related margins
*
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 09/27] drm: Add VRR target frame rate properties
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Userspace may wish to set a target frame rate for the Variable Refresh
Rate mechanism. There are multiple possible ways such a target could be
communicated.
It's desirable for userspace to not only set a fixed target, but a
target range within which VRR may operate. This is because even if a
sink advertises a certain range as supported, said range may not be the
ideal range to use, as numbers on the extreme end of the range may
induce flickering or other glitches.
There's four choices to how one bracketing value for the target range
could be communicated:
1. Target frame rate in something like millihertz
2. Target frame period in something like usecs
3. Target vtotal value
4. Target frame rate as a fraction expressed by two integers
Choice 1 is unpleasant as it would introduce some amount of rounding to
common broadcast framerates like 24/1.001, which seems minor except that
exact matches to standard framerates would rely on making all userspace
and kernel agree on a specific method of rounding. This is sure to go
wrong, so is disqualified.
Choice 2 has the same problem as 1, with the added complication that
the EDID's VRR range is expressed in frames per second, and other HDMI
features express things in frames per second as well, which would
require possibly imprecise computations.
Choice 3 is disqualified as mechanisms other than VRR, such as FVA, may
also modify the vtotal value. Additionally, it has the same drawbacks of
choice 2 and 1.
Choice 4 is therefore what's implemented. The frame rate 24/1.001Hz, for
example, is expressed as nominator=24000 and denominator=1001. As there
is both a minimum and a maximum, there are 4 properties in total: two
numerators, and two denominators. A fixed rate with no variability is
expressed by setting the minimum fractional and the maximum fractional
properties to the same value.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_atomic.c | 14 ++++++++++++
drivers/gpu/drm/drm_atomic_uapi.c | 24 ++++++++++++++++++++
drivers/gpu/drm/drm_connector.c | 48 +++++++++++++++++++++++++++++++++++++++
drivers/gpu/drm/drm_crtc.c | 34 +++++++++++++++++++++++++++
drivers/gpu/drm/drm_mode_config.c | 20 ++++++++++++++++
include/drm/drm_crtc.h | 6 +++++
include/drm/drm_mode_config.h | 45 ++++++++++++++++++++++++++++++++++++
7 files changed, 191 insertions(+)
diff --git a/drivers/gpu/drm/drm_atomic.c b/drivers/gpu/drm/drm_atomic.c
index 9bb761eda59f..da5c6925c349 100644
--- a/drivers/gpu/drm/drm_atomic.c
+++ b/drivers/gpu/drm/drm_atomic.c
@@ -489,6 +489,7 @@ EXPORT_SYMBOL(drm_atomic_get_crtc_state);
static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
const struct drm_crtc_state *new_crtc_state)
{
+ const struct drm_crtc_vrr_state *vrr = &new_crtc_state->vrr_state;
struct drm_crtc *crtc = new_crtc_state->crtc;
/* NOTE: we explicitly don't enforce constraints such as primary
@@ -544,6 +545,19 @@ static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
return -EINVAL;
}
+ /*
+ * Reject VRR limiter ranges that only specify one end of the limit, or
+ * specify a numerator > 0 with a denominator that is 0.
+ */
+ if ((!vrr->vrr_min_n || !vrr->vrr_min_d ||
+ !vrr->vrr_max_n || !vrr->vrr_max_d) &&
+ (vrr->vrr_min_n > 0 || vrr->vrr_max_n > 0)) {
+ drm_dbg_atomic(crtc->dev,
+ "[CRTC:%d:%s] incomplete VRR limiter range\n",
+ crtc->base.id, crtc->name);
+ return -EINVAL;
+ }
+
return 0;
}
diff --git a/drivers/gpu/drm/drm_atomic_uapi.c b/drivers/gpu/drm/drm_atomic_uapi.c
index 127bdd77802b..c95eaa9697c4 100644
--- a/drivers/gpu/drm/drm_atomic_uapi.c
+++ b/drivers/gpu/drm/drm_atomic_uapi.c
@@ -420,6 +420,22 @@ static int drm_atomic_crtc_set_property(struct drm_crtc *crtc,
return ret;
} else if (property == config->prop_vrr_enabled) {
state->vrr_enabled = val;
+ } else if (property == config->prop_vrr_min_numerator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_min_n = val;
+ } else if (property == config->prop_vrr_min_denominator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_min_d = val;
+ } else if (property == config->prop_vrr_max_numerator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_max_n = val;
+ } else if (property == config->prop_vrr_max_denominator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_max_d = val;
} else if (property == config->degamma_lut_property) {
const size_t elem_size = sizeof(struct drm_color_lut);
u64 lut_size;
@@ -505,6 +521,14 @@ drm_atomic_crtc_get_property(struct drm_crtc *crtc,
*val = (state->mode_blob) ? state->mode_blob->base.id : 0;
else if (property == config->prop_vrr_enabled)
*val = state->vrr_enabled;
+ else if (property == config->prop_vrr_min_numerator)
+ *val = state->vrr_state.vrr_min_n;
+ else if (property == config->prop_vrr_min_denominator)
+ *val = state->vrr_state.vrr_min_d;
+ else if (property == config->prop_vrr_max_numerator)
+ *val = state->vrr_state.vrr_max_n;
+ else if (property == config->prop_vrr_max_denominator)
+ *val = state->vrr_state.vrr_max_d;
else if (property == config->degamma_lut_property)
*val = (state->degamma_lut) ? state->degamma_lut->base.id : 0;
else if (property == config->ctm_property)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index a44afba8c5ff..33c4195db58b 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -2593,6 +2593,54 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* - Game - set display to game mode if display supports it,
* regardless of CRTC state
* - Off - set display to VRR disabled mode, regardless of CRTC state
+ *
+ * VRR Limiter/Target Properties
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The ``VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR}`` properties expose a mechanism
+ * through which userspace can control the desired range of refresh rates in
+ * which VRR is allowed to operate. Each rate is expressed as a
+ * numerator/denominator fraction of refresh rates in Hz, allowing for rational
+ * target rates like 24/1.001 Hz with no loss of precision or ambiguity.
+ *
+ * If the minimum and maximum rate are set to the same value (and not 0), they
+ * are understood as a fixed target rate. This is especially useful for media
+ * playback, where the content's frame rate is both constant and known in
+ * advance. In such cases, a refresh rate that is not an integer multiple of the
+ * content's frame rate will introduce judder, since not every frame is
+ * displayed for the same amount of time. A modeset of the display with a
+ * compatible rate may in those cases be either undesirable or impossible, but
+ * the rate can still effectively be reached through VRR.
+ *
+ * If one end of the limit is set (e.g. minimum), the other end (e.g. maximum)
+ * must also be set. Any rational with a numerator of 0 is treated as 0/0
+ * regardless of denominator.
+ *
+ * .. _VRR-MIN-NUMERATOR:
+ *
+ * "VRR_MIN_NUMERATOR":
+ * Default &drm_crtc integer property forming the numerator of a
+ * numerator/denominator pair of a frame rate to set as the minimum VRR
+ * target rate. Set to 0 to disable.
+ *
+ * "VRR_MIN_DENOMINATOR":
+ * Default &drm_crtc integer property forming the denominator of a
+ * numerator/denominator pair of a frame rate to set as the minimum VRR
+ * target rate. If :ref:`VRR_MIN_NUMERATOR <VRR-MIN-NUMERATOR>` is not
+ * zero, it must be non-zero.
+ *
+ * .. _VRR-MAX-NUMERATOR:
+ *
+ * "VRR_MAX_NUMERATOR":
+ * Default &drm_crtc integer property forming the numerator of a
+ * numerator/denominator pair of a frame rate to set as the maximum VRR
+ * target rate. Set to 0 to disable.
+ *
+ * "VRR_MAX_DENOMINATOR":
+ * Default &drm_crtc integer property forming the denominator of a
+ * numerator/denominator pair of a frame rate to set as the maximum VRR
+ * target rate. If :ref:`VRR_MAX_NUMERATOR <VRR-MAX-NUMERATOR>` is not
+ * zero, it must be non-zero.
*/
/**
diff --git a/drivers/gpu/drm/drm_crtc.c b/drivers/gpu/drm/drm_crtc.c
index 5295ec122e0d..4c29c6e15df9 100644
--- a/drivers/gpu/drm/drm_crtc.c
+++ b/drivers/gpu/drm/drm_crtc.c
@@ -324,6 +324,40 @@ static int __drm_crtc_init_with_planes(struct drm_device *dev, struct drm_crtc *
return 0;
}
+/**
+ * drm_crtc_attach_vrr_limiter_properties - Attach VRR limiter props to CRTC
+ * @crtc: The CRTC to attach the properties to
+ *
+ * Attach the VRR limiter target minimum and maximum frame rate properties to
+ * @crtc. The driver implementing @crtc must use atomic mode setting.
+ *
+ * This convenience function is to be used by drivers which implement all the
+ * logic required for the VRR limiter uAPI to function.
+ *
+ * Returns:
+ * Zero on success, negative error on failure.
+ */
+int drm_crtc_attach_vrr_limiter_properties(struct drm_crtc *crtc)
+{
+ struct drm_mode_config *config = &crtc->dev->mode_config;
+
+ if (!drm_drv_uses_atomic_modeset(crtc->dev))
+ return -EINVAL;
+
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_min_numerator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_min_denominator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_max_numerator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_max_denominator, 0);
+
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_crtc_attach_vrr_limiter_properties);
+
/**
* drm_crtc_init_with_planes - Initialise a new CRTC object with
* specified primary and cursor planes.
diff --git a/drivers/gpu/drm/drm_mode_config.c b/drivers/gpu/drm/drm_mode_config.c
index 48a56f23dadb..da967d60f832 100644
--- a/drivers/gpu/drm/drm_mode_config.c
+++ b/drivers/gpu/drm/drm_mode_config.c
@@ -535,6 +535,26 @@ static int drm_mode_create_standard_properties(struct drm_device *dev)
return -ENOMEM;
dev->mode_config.prop_vrr_enabled = prop;
+ prop = drm_property_create_range(dev, 0, "VRR_MIN_NUMERATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_min_numerator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MIN_DENOMINATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_min_denominator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MAX_NUMERATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_max_numerator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MAX_DENOMINATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_max_denominator = prop;
+
prop = drm_property_create(dev,
DRM_MODE_PROP_BLOB,
"DEGAMMA_LUT", 0);
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index bfb2b0741625..9004f7c2f21e 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -66,6 +66,10 @@ struct drm_plane_helper_funcs;
struct drm_crtc_vrr_state {
u16 cur_vtotal;
u16 max_vtotal;
+ u32 vrr_min_n;
+ u32 vrr_min_d;
+ u32 vrr_max_n;
+ u32 vrr_max_d;
u16 base_vtotal;
bool dynamic;
};
@@ -1247,6 +1251,8 @@ struct drm_mode_set {
#define obj_to_crtc(x) container_of(x, struct drm_crtc, base)
+int drm_crtc_attach_vrr_limiter_properties(struct drm_crtc *crtc);
+
__printf(6, 7)
int drm_crtc_init_with_planes(struct drm_device *dev,
struct drm_crtc *crtc,
diff --git a/include/drm/drm_mode_config.h b/include/drm/drm_mode_config.h
index 084517beb924..c8d5286f9267 100644
--- a/include/drm/drm_mode_config.h
+++ b/include/drm/drm_mode_config.h
@@ -702,6 +702,51 @@ struct drm_mode_config {
*/
struct drm_property *prop_vrr_enabled;
+ /**
+ * @prop_vrr_min_numerator: Default atomic CRTC property to indicate the
+ * numerator of a numerator/denominator variable refresh rate minimum
+ * target frame rate.
+ *
+ * If not zero, @prop_vrr_min_numerator divided by
+ * @prop_vrr_min_denominator must result in a frame rate above the
+ * sink's minimum VRR rate.
+ */
+ struct drm_property *prop_vrr_min_numerator;
+
+ /**
+ * @prop_vrr_min_denominator: Default atomic CRTC property to indicate
+ * the denominator of a numerator/denominator variable refresh rate
+ * minimum target frame rate.
+ *
+ * If @prop_vrr_min_numerator is set, @prop_vrr_min_denominator must not
+ * be 0, and @prop_vrr_min_numerator divided by @prop_vrr_min_denominator
+ * must result in a frame rate above the minimum VRR rate.
+ */
+ struct drm_property *prop_vrr_min_denominator;
+
+ /**
+ * @prop_vrr_max_numerator: Default atomic CRTC property to indicate the
+ * numerator of a numerator/denominator variable refresh rate maximum
+ * target frame rate range.
+ *
+ * The fraction expressed by this property divided by its corresponding
+ * denominator must be equal to or greater than the value of
+ * @prop_vrr_min_numerator.
+ */
+ struct drm_property *prop_vrr_max_numerator;
+
+ /**
+ * @prop_vrr_max_denominator: Default atomic CRTC property to indicate
+ * the denominator of a numerator/denominator variable refresh rate
+ * maximum target frame rate range.
+ *
+ * If @prop_vrr_max_numerator is set, @prop_vrr_max_denominator must not
+ * be 0, and @prop_vrr_max_numerator divided by @prop_vrr_max_denominator
+ * must result in a frame rate equal to or above @prop_vrr_min_numerator
+ * divided by @prop_vrr_min_denominator.
+ */
+ struct drm_property *prop_vrr_max_denominator;
+
/**
* @dvi_i_subconnector_property: Optional DVI-I property to
* differentiate between analog or digital mode.
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 09/27] drm: Add VRR target frame rate properties
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Userspace may wish to set a target frame rate for the Variable Refresh
Rate mechanism. There are multiple possible ways such a target could be
communicated.
It's desirable for userspace to not only set a fixed target, but a
target range within which VRR may operate. This is because even if a
sink advertises a certain range as supported, said range may not be the
ideal range to use, as numbers on the extreme end of the range may
induce flickering or other glitches.
There's four choices to how one bracketing value for the target range
could be communicated:
1. Target frame rate in something like millihertz
2. Target frame period in something like usecs
3. Target vtotal value
4. Target frame rate as a fraction expressed by two integers
Choice 1 is unpleasant as it would introduce some amount of rounding to
common broadcast framerates like 24/1.001, which seems minor except that
exact matches to standard framerates would rely on making all userspace
and kernel agree on a specific method of rounding. This is sure to go
wrong, so is disqualified.
Choice 2 has the same problem as 1, with the added complication that
the EDID's VRR range is expressed in frames per second, and other HDMI
features express things in frames per second as well, which would
require possibly imprecise computations.
Choice 3 is disqualified as mechanisms other than VRR, such as FVA, may
also modify the vtotal value. Additionally, it has the same drawbacks of
choice 2 and 1.
Choice 4 is therefore what's implemented. The frame rate 24/1.001Hz, for
example, is expressed as nominator=24000 and denominator=1001. As there
is both a minimum and a maximum, there are 4 properties in total: two
numerators, and two denominators. A fixed rate with no variability is
expressed by setting the minimum fractional and the maximum fractional
properties to the same value.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_atomic.c | 14 ++++++++++++
drivers/gpu/drm/drm_atomic_uapi.c | 24 ++++++++++++++++++++
drivers/gpu/drm/drm_connector.c | 48 +++++++++++++++++++++++++++++++++++++++
drivers/gpu/drm/drm_crtc.c | 34 +++++++++++++++++++++++++++
drivers/gpu/drm/drm_mode_config.c | 20 ++++++++++++++++
include/drm/drm_crtc.h | 6 +++++
include/drm/drm_mode_config.h | 45 ++++++++++++++++++++++++++++++++++++
7 files changed, 191 insertions(+)
diff --git a/drivers/gpu/drm/drm_atomic.c b/drivers/gpu/drm/drm_atomic.c
index 9bb761eda59f..da5c6925c349 100644
--- a/drivers/gpu/drm/drm_atomic.c
+++ b/drivers/gpu/drm/drm_atomic.c
@@ -489,6 +489,7 @@ EXPORT_SYMBOL(drm_atomic_get_crtc_state);
static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
const struct drm_crtc_state *new_crtc_state)
{
+ const struct drm_crtc_vrr_state *vrr = &new_crtc_state->vrr_state;
struct drm_crtc *crtc = new_crtc_state->crtc;
/* NOTE: we explicitly don't enforce constraints such as primary
@@ -544,6 +545,19 @@ static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
return -EINVAL;
}
+ /*
+ * Reject VRR limiter ranges that only specify one end of the limit, or
+ * specify a numerator > 0 with a denominator that is 0.
+ */
+ if ((!vrr->vrr_min_n || !vrr->vrr_min_d ||
+ !vrr->vrr_max_n || !vrr->vrr_max_d) &&
+ (vrr->vrr_min_n > 0 || vrr->vrr_max_n > 0)) {
+ drm_dbg_atomic(crtc->dev,
+ "[CRTC:%d:%s] incomplete VRR limiter range\n",
+ crtc->base.id, crtc->name);
+ return -EINVAL;
+ }
+
return 0;
}
diff --git a/drivers/gpu/drm/drm_atomic_uapi.c b/drivers/gpu/drm/drm_atomic_uapi.c
index 127bdd77802b..c95eaa9697c4 100644
--- a/drivers/gpu/drm/drm_atomic_uapi.c
+++ b/drivers/gpu/drm/drm_atomic_uapi.c
@@ -420,6 +420,22 @@ static int drm_atomic_crtc_set_property(struct drm_crtc *crtc,
return ret;
} else if (property == config->prop_vrr_enabled) {
state->vrr_enabled = val;
+ } else if (property == config->prop_vrr_min_numerator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_min_n = val;
+ } else if (property == config->prop_vrr_min_denominator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_min_d = val;
+ } else if (property == config->prop_vrr_max_numerator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_max_n = val;
+ } else if (property == config->prop_vrr_max_denominator) {
+ if (val > U32_MAX)
+ return -EINVAL;
+ state->vrr_state.vrr_max_d = val;
} else if (property == config->degamma_lut_property) {
const size_t elem_size = sizeof(struct drm_color_lut);
u64 lut_size;
@@ -505,6 +521,14 @@ drm_atomic_crtc_get_property(struct drm_crtc *crtc,
*val = (state->mode_blob) ? state->mode_blob->base.id : 0;
else if (property == config->prop_vrr_enabled)
*val = state->vrr_enabled;
+ else if (property == config->prop_vrr_min_numerator)
+ *val = state->vrr_state.vrr_min_n;
+ else if (property == config->prop_vrr_min_denominator)
+ *val = state->vrr_state.vrr_min_d;
+ else if (property == config->prop_vrr_max_numerator)
+ *val = state->vrr_state.vrr_max_n;
+ else if (property == config->prop_vrr_max_denominator)
+ *val = state->vrr_state.vrr_max_d;
else if (property == config->degamma_lut_property)
*val = (state->degamma_lut) ? state->degamma_lut->base.id : 0;
else if (property == config->ctm_property)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index a44afba8c5ff..33c4195db58b 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -2593,6 +2593,54 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* - Game - set display to game mode if display supports it,
* regardless of CRTC state
* - Off - set display to VRR disabled mode, regardless of CRTC state
+ *
+ * VRR Limiter/Target Properties
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The ``VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR}`` properties expose a mechanism
+ * through which userspace can control the desired range of refresh rates in
+ * which VRR is allowed to operate. Each rate is expressed as a
+ * numerator/denominator fraction of refresh rates in Hz, allowing for rational
+ * target rates like 24/1.001 Hz with no loss of precision or ambiguity.
+ *
+ * If the minimum and maximum rate are set to the same value (and not 0), they
+ * are understood as a fixed target rate. This is especially useful for media
+ * playback, where the content's frame rate is both constant and known in
+ * advance. In such cases, a refresh rate that is not an integer multiple of the
+ * content's frame rate will introduce judder, since not every frame is
+ * displayed for the same amount of time. A modeset of the display with a
+ * compatible rate may in those cases be either undesirable or impossible, but
+ * the rate can still effectively be reached through VRR.
+ *
+ * If one end of the limit is set (e.g. minimum), the other end (e.g. maximum)
+ * must also be set. Any rational with a numerator of 0 is treated as 0/0
+ * regardless of denominator.
+ *
+ * .. _VRR-MIN-NUMERATOR:
+ *
+ * "VRR_MIN_NUMERATOR":
+ * Default &drm_crtc integer property forming the numerator of a
+ * numerator/denominator pair of a frame rate to set as the minimum VRR
+ * target rate. Set to 0 to disable.
+ *
+ * "VRR_MIN_DENOMINATOR":
+ * Default &drm_crtc integer property forming the denominator of a
+ * numerator/denominator pair of a frame rate to set as the minimum VRR
+ * target rate. If :ref:`VRR_MIN_NUMERATOR <VRR-MIN-NUMERATOR>` is not
+ * zero, it must be non-zero.
+ *
+ * .. _VRR-MAX-NUMERATOR:
+ *
+ * "VRR_MAX_NUMERATOR":
+ * Default &drm_crtc integer property forming the numerator of a
+ * numerator/denominator pair of a frame rate to set as the maximum VRR
+ * target rate. Set to 0 to disable.
+ *
+ * "VRR_MAX_DENOMINATOR":
+ * Default &drm_crtc integer property forming the denominator of a
+ * numerator/denominator pair of a frame rate to set as the maximum VRR
+ * target rate. If :ref:`VRR_MAX_NUMERATOR <VRR-MAX-NUMERATOR>` is not
+ * zero, it must be non-zero.
*/
/**
diff --git a/drivers/gpu/drm/drm_crtc.c b/drivers/gpu/drm/drm_crtc.c
index 5295ec122e0d..4c29c6e15df9 100644
--- a/drivers/gpu/drm/drm_crtc.c
+++ b/drivers/gpu/drm/drm_crtc.c
@@ -324,6 +324,40 @@ static int __drm_crtc_init_with_planes(struct drm_device *dev, struct drm_crtc *
return 0;
}
+/**
+ * drm_crtc_attach_vrr_limiter_properties - Attach VRR limiter props to CRTC
+ * @crtc: The CRTC to attach the properties to
+ *
+ * Attach the VRR limiter target minimum and maximum frame rate properties to
+ * @crtc. The driver implementing @crtc must use atomic mode setting.
+ *
+ * This convenience function is to be used by drivers which implement all the
+ * logic required for the VRR limiter uAPI to function.
+ *
+ * Returns:
+ * Zero on success, negative error on failure.
+ */
+int drm_crtc_attach_vrr_limiter_properties(struct drm_crtc *crtc)
+{
+ struct drm_mode_config *config = &crtc->dev->mode_config;
+
+ if (!drm_drv_uses_atomic_modeset(crtc->dev))
+ return -EINVAL;
+
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_min_numerator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_min_denominator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_max_numerator, 0);
+ drm_object_attach_property(&crtc->base,
+ config->prop_vrr_max_denominator, 0);
+
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_crtc_attach_vrr_limiter_properties);
+
/**
* drm_crtc_init_with_planes - Initialise a new CRTC object with
* specified primary and cursor planes.
diff --git a/drivers/gpu/drm/drm_mode_config.c b/drivers/gpu/drm/drm_mode_config.c
index 48a56f23dadb..da967d60f832 100644
--- a/drivers/gpu/drm/drm_mode_config.c
+++ b/drivers/gpu/drm/drm_mode_config.c
@@ -535,6 +535,26 @@ static int drm_mode_create_standard_properties(struct drm_device *dev)
return -ENOMEM;
dev->mode_config.prop_vrr_enabled = prop;
+ prop = drm_property_create_range(dev, 0, "VRR_MIN_NUMERATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_min_numerator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MIN_DENOMINATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_min_denominator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MAX_NUMERATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_max_numerator = prop;
+
+ prop = drm_property_create_range(dev, 0, "VRR_MAX_DENOMINATOR", 0, U32_MAX);
+ if (!prop)
+ return -ENOMEM;
+ dev->mode_config.prop_vrr_max_denominator = prop;
+
prop = drm_property_create(dev,
DRM_MODE_PROP_BLOB,
"DEGAMMA_LUT", 0);
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index bfb2b0741625..9004f7c2f21e 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -66,6 +66,10 @@ struct drm_plane_helper_funcs;
struct drm_crtc_vrr_state {
u16 cur_vtotal;
u16 max_vtotal;
+ u32 vrr_min_n;
+ u32 vrr_min_d;
+ u32 vrr_max_n;
+ u32 vrr_max_d;
u16 base_vtotal;
bool dynamic;
};
@@ -1247,6 +1251,8 @@ struct drm_mode_set {
#define obj_to_crtc(x) container_of(x, struct drm_crtc, base)
+int drm_crtc_attach_vrr_limiter_properties(struct drm_crtc *crtc);
+
__printf(6, 7)
int drm_crtc_init_with_planes(struct drm_device *dev,
struct drm_crtc *crtc,
diff --git a/include/drm/drm_mode_config.h b/include/drm/drm_mode_config.h
index 084517beb924..c8d5286f9267 100644
--- a/include/drm/drm_mode_config.h
+++ b/include/drm/drm_mode_config.h
@@ -702,6 +702,51 @@ struct drm_mode_config {
*/
struct drm_property *prop_vrr_enabled;
+ /**
+ * @prop_vrr_min_numerator: Default atomic CRTC property to indicate the
+ * numerator of a numerator/denominator variable refresh rate minimum
+ * target frame rate.
+ *
+ * If not zero, @prop_vrr_min_numerator divided by
+ * @prop_vrr_min_denominator must result in a frame rate above the
+ * sink's minimum VRR rate.
+ */
+ struct drm_property *prop_vrr_min_numerator;
+
+ /**
+ * @prop_vrr_min_denominator: Default atomic CRTC property to indicate
+ * the denominator of a numerator/denominator variable refresh rate
+ * minimum target frame rate.
+ *
+ * If @prop_vrr_min_numerator is set, @prop_vrr_min_denominator must not
+ * be 0, and @prop_vrr_min_numerator divided by @prop_vrr_min_denominator
+ * must result in a frame rate above the minimum VRR rate.
+ */
+ struct drm_property *prop_vrr_min_denominator;
+
+ /**
+ * @prop_vrr_max_numerator: Default atomic CRTC property to indicate the
+ * numerator of a numerator/denominator variable refresh rate maximum
+ * target frame rate range.
+ *
+ * The fraction expressed by this property divided by its corresponding
+ * denominator must be equal to or greater than the value of
+ * @prop_vrr_min_numerator.
+ */
+ struct drm_property *prop_vrr_max_numerator;
+
+ /**
+ * @prop_vrr_max_denominator: Default atomic CRTC property to indicate
+ * the denominator of a numerator/denominator variable refresh rate
+ * maximum target frame rate range.
+ *
+ * If @prop_vrr_max_numerator is set, @prop_vrr_max_denominator must not
+ * be 0, and @prop_vrr_max_numerator divided by @prop_vrr_max_denominator
+ * must result in a frame rate equal to or above @prop_vrr_min_numerator
+ * divided by @prop_vrr_min_denominator.
+ */
+ struct drm_property *prop_vrr_max_denominator;
+
/**
* @dvi_i_subconnector_property: Optional DVI-I property to
* differentiate between analog or digital mode.
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 10/27] drm/connector: Add vrr_display_{min,max} connector properties
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Depending on whichever quirks a driver applies, what the minimum and
maximum display-supported refresh rates for VRR are is not always
obvious to userspace.
To rectify this, add two new read-only connector properties. These
properties specify the smallest, and respectively the largest, integer
refresh rate in Hertz that the current mode is capable of displaying
with Variable Refresh Rate.
Due to how messy and chicken-and-egg it would be to implement it, this
does not consider the fractional rates below VRR min that might be
allowed, the CinemaVRR rates that might be allowed below VRRmin on HDMI
displays with this feature if userspace ends up presenting fixed rate,
or the QMS range.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_helper.c | 55 +++++++++++++++++++++++++
drivers/gpu/drm/drm_connector.c | 67 +++++++++++++++++++++++++++++++
include/drm/display/drm_hdmi_helper.h | 3 ++
include/drm/drm_connector.h | 30 ++++++++++++++
4 files changed, 155 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_helper.c b/drivers/gpu/drm/display/drm_hdmi_helper.c
index f85cce3d6254..8f325d5e496d 100644
--- a/drivers/gpu/drm/display/drm_hdmi_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_helper.c
@@ -730,3 +730,58 @@ int drm_connector_hdmi_sync_scdc(struct drm_connector *connector, bool plugged,
return ret;
}
EXPORT_SYMBOL(drm_connector_hdmi_sync_scdc);
+
+/**
+ * drm_connector_hdmi_update_vrr_limits - update VRR display limit properties
+ * @conn: pointer to the &struct drm_connector to set the properties on
+ * @mode: pointer to the &struct drm_display_mode to consider while updating
+ *
+ * Update the "vrr_display_min" and "vrr_display_max" properties on @conn based
+ * on its HDMI sink info and the current mode. Does not consider any display
+ * protocol other than HDMI.
+ *
+ * Will set both min and max to 0 if max happens to end up below min, which can
+ * happen with CNMVRR==0 and a lower refresh rate @mode.
+ *
+ * Return: %0 on success, negative errno otherwise
+ */
+int drm_connector_hdmi_update_vrr_limits(struct drm_connector *conn,
+ const struct drm_display_mode *mode)
+{
+ struct drm_mode_object *obj = &conn->base;
+ const struct drm_display_info *info;
+ u16 min_hz, max_hz;
+
+ if (!conn->vrr_display_min_property || !conn->vrr_display_max_property)
+ return -ENOENT;
+
+ info = &conn->display_info;
+
+ if (!info->is_hdmi)
+ return -EINVAL;
+
+ if (conn->status != connector_status_connected ||
+ !info->hdmi.vrr_cap.supported) {
+ drm_object_property_set_value(obj, conn->vrr_display_min_property, 0);
+ drm_object_property_set_value(obj, conn->vrr_display_max_property, 0);
+
+ return 0;
+ }
+
+ min_hz = info->hdmi.vrr_cap.vrr_min;
+
+ if (info->hdmi.vrr_cap.cnmvrr)
+ max_hz = info->hdmi.vrr_cap.vrr_max;
+ else
+ max_hz = min(drm_mode_vrefresh(mode), info->hdmi.vrr_cap.vrr_max);
+
+ /* VRR is impossible in the current mode */
+ if (max_hz <= min_hz)
+ min_hz = max_hz = 0;
+
+ drm_object_property_set_value(obj, conn->vrr_display_min_property, min_hz);
+ drm_object_property_set_value(obj, conn->vrr_display_max_property, max_hz);
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_hdmi_update_vrr_limits);
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 33c4195db58b..21222214a843 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -2641,6 +2641,20 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* numerator/denominator pair of a frame rate to set as the maximum VRR
* target rate. If :ref:`VRR_MAX_NUMERATOR <VRR-MAX-NUMERATOR>` is not
* zero, it must be non-zero.
+ *
+ * "vrr_display_min":
+ * Optional &drm_connector property that expresses the lowest integer
+ * frame rate in Hertz that the display can achieve in the current mode
+ * with VRR. It is highly likely the display can represent rational rates
+ * below this value (e.g. 24/1.001), which an atomic test commit with the
+ * limiters set appropriately will reveal. This minimum is assuming
+ * non-constant frame intervals, i.e. does not consider features like
+ * CinemaVRR for HDMI.
+ *
+ * "vrr_display_max":
+ * Optional &drm_connector property that expresses the highest integer
+ * frame rate in Hertz that the display can achieve in the current mode
+ * with VRR.
*/
/**
@@ -2737,6 +2751,59 @@ int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connect
}
EXPORT_SYMBOL(drm_connector_attach_vrr_display_mode_property);
+/**
+ * drm_connector_attach_vrr_display_limits_properties - creates and attaches VRR
+ * display limit properties
+ * @connector: pointer to the &struct drm_connector to attach the properties to
+ *
+ * Creates and attaches the "vrr_display_min" and "vrr_display_max" properties
+ * on @connector. Will either create and attach both, or on failure, neither,
+ * but never just one of the two, unless a connector with just one of the two
+ * already created is passed in.
+ *
+ * Returns:
+ * Zero on success, negative errno on failure
+ */
+int drm_connector_attach_vrr_display_limits_properties(
+ struct drm_connector *connector)
+{
+ struct drm_device *dev = connector->dev;
+ struct drm_property *min_p = NULL, *max_p = NULL;
+
+ if (!connector->vrr_display_min_property) {
+ min_p = drm_property_create_range(dev, DRM_MODE_PROP_IMMUTABLE,
+ "vrr_display_min", 0, U16_MAX);
+
+ if (!min_p)
+ return -ENOMEM;
+ }
+
+ if (!connector->vrr_display_max_property) {
+ max_p = drm_property_create_range(dev, DRM_MODE_PROP_IMMUTABLE,
+ "vrr_display_max", 0, U16_MAX);
+
+ if (!max_p) {
+ if (min_p)
+ drm_property_destroy(dev, min_p);
+
+ return -ENOMEM;
+ }
+ }
+
+ if (min_p) {
+ connector->vrr_display_min_property = min_p;
+ drm_object_attach_property(&connector->base, min_p, 0);
+ }
+
+ if (max_p) {
+ connector->vrr_display_max_property = max_p;
+ drm_object_attach_property(&connector->base, max_p, 0);
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_attach_vrr_display_limits_properties);
+
/**
* drm_connector_attach_scaling_mode_property - attach atomic scaling mode property
* @connector: connector to attach scaling mode property on.
diff --git a/include/drm/display/drm_hdmi_helper.h b/include/drm/display/drm_hdmi_helper.h
index 2cdd6e3bc090..dd96885bb592 100644
--- a/include/drm/display/drm_hdmi_helper.h
+++ b/include/drm/display/drm_hdmi_helper.h
@@ -50,4 +50,7 @@ int
drm_connector_hdmi_sync_scdc(struct drm_connector *connector, bool plugged,
struct drm_modeset_acquire_ctx *ctx);
+int drm_connector_hdmi_update_vrr_limits(struct drm_connector *conn,
+ const struct drm_display_mode *mode);
+
#endif
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 5b6920c1a996..c0865a2547fd 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -2495,6 +2495,34 @@ struct drm_connector {
*/
struct drm_property *vrr_display_mode_property;
+ /**
+ * @vrr_display_min_property: Optional read-only property to help
+ * userspace query the minimum supported VRR refresh rate in Hz by the
+ * display.
+ *
+ * Can be attached to a connector by calling
+ * drm_connector_attach_vrr_display_limits_properties().
+ *
+ * Drivers should update it through drm_object_property_set_value()
+ * or through the helper drm_connector_hdmi_update_vrr_limits() for HDMI
+ * connectors using standard HDMI VRR.
+ */
+ struct drm_property *vrr_display_min_property;
+
+ /**
+ * @vrr_display_max_property: Optional read-only property to help
+ * userspace query the maximum supported VRR refresh rate in Hz by the
+ * display.
+ *
+ * Can be attached to a connector by calling
+ * drm_connector_attach_vrr_display_limits_properties().
+ *
+ * Drivers should update it through drm_object_property_set_value()
+ * or through the helper drm_connector_hdmi_update_vrr_limits() for HDMI
+ * connectors using standard HDMI VRR.
+ */
+ struct drm_property *vrr_display_max_property;
+
/**
* @colorspace_property: Connector property to set the suitable
* colorspace supported by the sink.
@@ -2927,6 +2955,8 @@ int drm_connector_attach_vrr_capable_property(
struct drm_connector *connector);
int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
unsigned int supported_vrr_modes);
+int drm_connector_attach_vrr_display_limits_properties(
+ struct drm_connector *connector);
void drm_connector_attach_panel_type_property(struct drm_connector *connector);
int drm_connector_attach_broadcast_rgb_property(struct drm_connector *connector);
int drm_connector_attach_colorspace_property(struct drm_connector *connector);
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 10/27] drm/connector: Add vrr_display_{min,max} connector properties
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Depending on whichever quirks a driver applies, what the minimum and
maximum display-supported refresh rates for VRR are is not always
obvious to userspace.
To rectify this, add two new read-only connector properties. These
properties specify the smallest, and respectively the largest, integer
refresh rate in Hertz that the current mode is capable of displaying
with Variable Refresh Rate.
Due to how messy and chicken-and-egg it would be to implement it, this
does not consider the fractional rates below VRR min that might be
allowed, the CinemaVRR rates that might be allowed below VRRmin on HDMI
displays with this feature if userspace ends up presenting fixed rate,
or the QMS range.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_helper.c | 55 +++++++++++++++++++++++++
drivers/gpu/drm/drm_connector.c | 67 +++++++++++++++++++++++++++++++
include/drm/display/drm_hdmi_helper.h | 3 ++
include/drm/drm_connector.h | 30 ++++++++++++++
4 files changed, 155 insertions(+)
diff --git a/drivers/gpu/drm/display/drm_hdmi_helper.c b/drivers/gpu/drm/display/drm_hdmi_helper.c
index f85cce3d6254..8f325d5e496d 100644
--- a/drivers/gpu/drm/display/drm_hdmi_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_helper.c
@@ -730,3 +730,58 @@ int drm_connector_hdmi_sync_scdc(struct drm_connector *connector, bool plugged,
return ret;
}
EXPORT_SYMBOL(drm_connector_hdmi_sync_scdc);
+
+/**
+ * drm_connector_hdmi_update_vrr_limits - update VRR display limit properties
+ * @conn: pointer to the &struct drm_connector to set the properties on
+ * @mode: pointer to the &struct drm_display_mode to consider while updating
+ *
+ * Update the "vrr_display_min" and "vrr_display_max" properties on @conn based
+ * on its HDMI sink info and the current mode. Does not consider any display
+ * protocol other than HDMI.
+ *
+ * Will set both min and max to 0 if max happens to end up below min, which can
+ * happen with CNMVRR==0 and a lower refresh rate @mode.
+ *
+ * Return: %0 on success, negative errno otherwise
+ */
+int drm_connector_hdmi_update_vrr_limits(struct drm_connector *conn,
+ const struct drm_display_mode *mode)
+{
+ struct drm_mode_object *obj = &conn->base;
+ const struct drm_display_info *info;
+ u16 min_hz, max_hz;
+
+ if (!conn->vrr_display_min_property || !conn->vrr_display_max_property)
+ return -ENOENT;
+
+ info = &conn->display_info;
+
+ if (!info->is_hdmi)
+ return -EINVAL;
+
+ if (conn->status != connector_status_connected ||
+ !info->hdmi.vrr_cap.supported) {
+ drm_object_property_set_value(obj, conn->vrr_display_min_property, 0);
+ drm_object_property_set_value(obj, conn->vrr_display_max_property, 0);
+
+ return 0;
+ }
+
+ min_hz = info->hdmi.vrr_cap.vrr_min;
+
+ if (info->hdmi.vrr_cap.cnmvrr)
+ max_hz = info->hdmi.vrr_cap.vrr_max;
+ else
+ max_hz = min(drm_mode_vrefresh(mode), info->hdmi.vrr_cap.vrr_max);
+
+ /* VRR is impossible in the current mode */
+ if (max_hz <= min_hz)
+ min_hz = max_hz = 0;
+
+ drm_object_property_set_value(obj, conn->vrr_display_min_property, min_hz);
+ drm_object_property_set_value(obj, conn->vrr_display_max_property, max_hz);
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_hdmi_update_vrr_limits);
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 33c4195db58b..21222214a843 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -2641,6 +2641,20 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* numerator/denominator pair of a frame rate to set as the maximum VRR
* target rate. If :ref:`VRR_MAX_NUMERATOR <VRR-MAX-NUMERATOR>` is not
* zero, it must be non-zero.
+ *
+ * "vrr_display_min":
+ * Optional &drm_connector property that expresses the lowest integer
+ * frame rate in Hertz that the display can achieve in the current mode
+ * with VRR. It is highly likely the display can represent rational rates
+ * below this value (e.g. 24/1.001), which an atomic test commit with the
+ * limiters set appropriately will reveal. This minimum is assuming
+ * non-constant frame intervals, i.e. does not consider features like
+ * CinemaVRR for HDMI.
+ *
+ * "vrr_display_max":
+ * Optional &drm_connector property that expresses the highest integer
+ * frame rate in Hertz that the display can achieve in the current mode
+ * with VRR.
*/
/**
@@ -2737,6 +2751,59 @@ int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connect
}
EXPORT_SYMBOL(drm_connector_attach_vrr_display_mode_property);
+/**
+ * drm_connector_attach_vrr_display_limits_properties - creates and attaches VRR
+ * display limit properties
+ * @connector: pointer to the &struct drm_connector to attach the properties to
+ *
+ * Creates and attaches the "vrr_display_min" and "vrr_display_max" properties
+ * on @connector. Will either create and attach both, or on failure, neither,
+ * but never just one of the two, unless a connector with just one of the two
+ * already created is passed in.
+ *
+ * Returns:
+ * Zero on success, negative errno on failure
+ */
+int drm_connector_attach_vrr_display_limits_properties(
+ struct drm_connector *connector)
+{
+ struct drm_device *dev = connector->dev;
+ struct drm_property *min_p = NULL, *max_p = NULL;
+
+ if (!connector->vrr_display_min_property) {
+ min_p = drm_property_create_range(dev, DRM_MODE_PROP_IMMUTABLE,
+ "vrr_display_min", 0, U16_MAX);
+
+ if (!min_p)
+ return -ENOMEM;
+ }
+
+ if (!connector->vrr_display_max_property) {
+ max_p = drm_property_create_range(dev, DRM_MODE_PROP_IMMUTABLE,
+ "vrr_display_max", 0, U16_MAX);
+
+ if (!max_p) {
+ if (min_p)
+ drm_property_destroy(dev, min_p);
+
+ return -ENOMEM;
+ }
+ }
+
+ if (min_p) {
+ connector->vrr_display_min_property = min_p;
+ drm_object_attach_property(&connector->base, min_p, 0);
+ }
+
+ if (max_p) {
+ connector->vrr_display_max_property = max_p;
+ drm_object_attach_property(&connector->base, max_p, 0);
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL(drm_connector_attach_vrr_display_limits_properties);
+
/**
* drm_connector_attach_scaling_mode_property - attach atomic scaling mode property
* @connector: connector to attach scaling mode property on.
diff --git a/include/drm/display/drm_hdmi_helper.h b/include/drm/display/drm_hdmi_helper.h
index 2cdd6e3bc090..dd96885bb592 100644
--- a/include/drm/display/drm_hdmi_helper.h
+++ b/include/drm/display/drm_hdmi_helper.h
@@ -50,4 +50,7 @@ int
drm_connector_hdmi_sync_scdc(struct drm_connector *connector, bool plugged,
struct drm_modeset_acquire_ctx *ctx);
+int drm_connector_hdmi_update_vrr_limits(struct drm_connector *conn,
+ const struct drm_display_mode *mode);
+
#endif
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 5b6920c1a996..c0865a2547fd 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -2495,6 +2495,34 @@ struct drm_connector {
*/
struct drm_property *vrr_display_mode_property;
+ /**
+ * @vrr_display_min_property: Optional read-only property to help
+ * userspace query the minimum supported VRR refresh rate in Hz by the
+ * display.
+ *
+ * Can be attached to a connector by calling
+ * drm_connector_attach_vrr_display_limits_properties().
+ *
+ * Drivers should update it through drm_object_property_set_value()
+ * or through the helper drm_connector_hdmi_update_vrr_limits() for HDMI
+ * connectors using standard HDMI VRR.
+ */
+ struct drm_property *vrr_display_min_property;
+
+ /**
+ * @vrr_display_max_property: Optional read-only property to help
+ * userspace query the maximum supported VRR refresh rate in Hz by the
+ * display.
+ *
+ * Can be attached to a connector by calling
+ * drm_connector_attach_vrr_display_limits_properties().
+ *
+ * Drivers should update it through drm_object_property_set_value()
+ * or through the helper drm_connector_hdmi_update_vrr_limits() for HDMI
+ * connectors using standard HDMI VRR.
+ */
+ struct drm_property *vrr_display_max_property;
+
/**
* @colorspace_property: Connector property to set the suitable
* colorspace supported by the sink.
@@ -2927,6 +2955,8 @@ int drm_connector_attach_vrr_capable_property(
struct drm_connector *connector);
int drm_connector_attach_vrr_display_mode_property(struct drm_connector *connector,
unsigned int supported_vrr_modes);
+int drm_connector_attach_vrr_display_limits_properties(
+ struct drm_connector *connector);
void drm_connector_attach_panel_type_property(struct drm_connector *connector);
int drm_connector_attach_broadcast_rgb_property(struct drm_connector *connector);
int drm_connector_attach_colorspace_property(struct drm_connector *connector);
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 11/27] drm: Implement VRR rate limiting
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Currently, VRR is allowed to use the entire range advertised by the
sink's EDID. Userspace may find it desirable, however, to limit this
range, or set a fixed target rate entirely. The latter is mainly useful
for multimedia playback applications, where judder can be avoided by
matching the content's frame rate precisely.
With the new VRR target frame rate properties, the necessary
infrastructure is in place to allow implementing this.
Add the necessary logic to hdmi_validate_vrr, as well as the VRR
helpers. Since not every target rate can be reached precisely, the
per-frame error is calculated, and dithered one row at a time across
subsequent frames. This means some frames are one vtotal larger than
others, so that averaged over the frames, the desired target rate is
achieved. Such dithering is explicitly permitted by HDMI, and is still
considered a constant target frame rate.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 134 +++++++++++++++++++++---
drivers/gpu/drm/drm_crtc_helper.c | 34 +++++-
include/drm/drm_crtc.h | 3 +
include/drm/drm_crtc_helper.h | 2 +
4 files changed, 156 insertions(+), 17 deletions(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index ce680eaa5047..846f7a72fddc 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -5,6 +5,7 @@
#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
+#include <drm/drm_crtc_helper.h>
#include <drm/drm_edid.h>
#include <drm/drm_modes.h>
#include <drm/drm_print.h>
@@ -1051,15 +1052,60 @@ static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
return 0;
}
+static int hdmi_validate_vrr_limits(struct drm_crtc_state *crtc_state, u16 vrr_min)
+{
+ struct drm_crtc_vrr_state *vrr_state = &crtc_state->vrr_state;
+ const struct drm_display_mode *mode = &crtc_state->mode;
+ u32 limit_d;
+ u8 round;
+ int ret;
+
+ /* No limiter, don't need to validate, but reset the error */
+ if (!drm_crtc_helper_vrr_is_fixed_rate(vrr_state)) {
+ vrr_state->per_frame_err = 0;
+ vrr_state->cur_err = 0;
+ return 0;
+ }
+
+ /* Don't allow rates equal to or below the minimum VRR rate / 1.001 */
+ if (cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ vrr_min * 1000, 1001) < 0)
+ return -EINVAL;
+
+ if (check_mul_overflow(mode->crtc_htotal, vrr_state->vrr_min_n, &limit_d))
+ return -ERANGE;
+
+ ret = hdmi_calculate_vtotal(&crtc_state->mode, vrr_state->vrr_min_n,
+ vrr_state->vrr_min_d, &vrr_state->base_vtotal,
+ &vrr_state->denom, &vrr_state->per_frame_err);
+ if (ret)
+ return ret;
+
+ /* Allow rounding up for the vtotal check below */
+ if (vrr_state->per_frame_err >= vrr_state->denom / 2)
+ round = 1;
+ else
+ round = 0;
+
+ if (vrr_state->base_vtotal + round < mode->crtc_vtotal)
+ return -EINVAL;
+
+ return 0;
+}
+
static int hdmi_validate_vrr(struct drm_connector *connector,
struct drm_atomic_commit *state)
{
struct drm_connector_state *new_conn_state =
drm_atomic_get_new_connector_state(state, connector);
+ struct drm_connector_state *old_conn_state =
+ drm_atomic_get_old_connector_state(state, connector);
struct drm_crtc_state *new_crtc_state =
drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
struct drm_crtc_vrr_state *vrr_state =
&new_crtc_state->vrr_state;
+ struct drm_crtc_vrr_state *old_vrr_state;
+ struct drm_crtc_state *old_crtc_state = NULL;
struct drm_display_info *info = &connector->display_info;
const struct drm_display_mode *mode = &new_crtc_state->mode;
struct drm_device *dev = connector->dev;
@@ -1068,10 +1114,14 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
u32 min_vfreq_d = 1;
u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
u32 max_vfreq_d = 1;
+ bool is_fixed;
if (!new_crtc_state->vrr_enabled)
return 0;
+ if (old_conn_state)
+ old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
+
if (!info->hdmi.vrr_cap.supported)
return -EOPNOTSUPP;
@@ -1087,6 +1137,21 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
if (!max_vfreq_n || max_vfreq_n > mode_refresh)
max_vfreq_n = mode_refresh;
+ is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
+ if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
+ min_vfreq_n = 24;
+
+ /* If the VRR target changed, make sure to reset the error accumulator */
+ if (old_crtc_state) {
+ old_vrr_state = &old_crtc_state->vrr_state;
+ if (drm_crtc_helper_vrr_is_fixed_rate(old_vrr_state) != is_fixed ||
+ cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ old_vrr_state->vrr_min_n, old_vrr_state->vrr_min_d) ||
+ cmp_fraction(vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ old_vrr_state->vrr_max_n, old_vrr_state->vrr_max_d))
+ vrr_state->cur_err = 0;
+ }
+
if (!min_vfreq_n) {
drm_dbg_kms(dev, "Could not get minimum VRR rate from sink\n");
return -EINVAL;
@@ -1098,25 +1163,64 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
return -EINVAL;
}
- if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
- drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
- max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
- return -EINVAL;
- }
-
- ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
- &vrr_state->base_vtotal, NULL, NULL);
+ ret = hdmi_validate_vrr_limits(new_crtc_state, min_vfreq_n);
if (ret) {
- drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ drm_dbg_kms(dev, "Invalid VRR fixed target rate: %pe\n", ERR_PTR(ret));
return ret;
}
- /*
- * Even at its fastest, it can't go faster than the mode, so
- * clamp to avoid imprecisely rounded mode_refresh values we
- * can't do anything about from ruining our day.
- */
- vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
+ if (!is_fixed) {
+ vrr_state->dynamic = true;
+
+ if (vrr_state->vrr_min_n) {
+ if (!vrr_state->vrr_min_d)
+ return -EINVAL;
+
+ if (cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ min_vfreq_n * 1000, 1001) < 0) {
+ drm_dbg_kms(dev, "Target min (%u/%u) < supported min (%u/%u)\n",
+ vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+ min_vfreq_n = vrr_state->vrr_min_n;
+ min_vfreq_d = vrr_state->vrr_min_d;
+ }
+ if (vrr_state->vrr_max_n) {
+ if (!vrr_state->vrr_max_d)
+ return -EINVAL;
+
+ if (cmp_fraction(vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > supported max (%u/%u)\n",
+ vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ max_vfreq_n, max_vfreq_d);
+ return -EINVAL;
+ }
+ max_vfreq_n = vrr_state->vrr_max_n;
+ max_vfreq_d = vrr_state->vrr_max_d;
+ }
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
+ max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+
+ ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
+ &vrr_state->base_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ /*
+ * Even at its fastest, it can't go faster than the mode, so
+ * clamp to avoid imprecisely rounded mode_refresh values we
+ * can't do anything about from ruining our day.
+ */
+ vrr_state->base_vtotal = max(vrr_state->base_vtotal,
+ mode->crtc_vtotal);
+ }
if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
/* refresh <= VRR min, don't do VRR vtotal adjustment */
diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
index 4ffb3b0b7878..dd696f0c5016 100644
--- a/drivers/gpu/drm/drm_crtc_helper.c
+++ b/drivers/gpu/drm/drm_crtc_helper.c
@@ -838,6 +838,22 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
}
EXPORT_SYMBOL(drm_crtc_helper_set_config);
+bool drm_crtc_helper_vrr_is_fixed_rate(const struct drm_crtc_vrr_state *vrr)
+{
+ if (!vrr->vrr_min_n)
+ return false;
+
+ if (!vrr->vrr_min_d || !vrr->vrr_max_d)
+ return false;
+
+ if (mul_u32_u32(vrr->vrr_min_n, vrr->vrr_max_d) ==
+ mul_u32_u32(vrr->vrr_min_d, vrr->vrr_max_n))
+ return true;
+
+ return false;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_is_fixed_rate);
+
/**
* drm_crtc_helper_vrr_flush - Update VRR state on frame ready
* @crtc_state: pointer to &struct drm_crtc_state of CRTC that has a new frame ready
@@ -864,6 +880,9 @@ bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state)
return false;
}
+ if (drm_crtc_helper_vrr_is_fixed_rate(vrr))
+ return false;
+
if (vrr->cur_vtotal == vrr->base_vtotal)
return false;
@@ -889,10 +908,21 @@ bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
u16 old_vtotal = vrr->cur_vtotal;
- if (!crtc_state->vrr_enabled)
+ if (!crtc_state->vrr_enabled) {
vrr->cur_vtotal = 0;
- else
+ vrr->cur_err = 0;
+ } else if (drm_crtc_helper_vrr_is_fixed_rate(vrr)) {
+ if (vrr->cur_err >= (int) (vrr->denom / 2)) {
+ vrr->cur_err -= vrr->denom;
+ vrr->cur_vtotal = vrr->base_vtotal + 1;
+ } else {
+ vrr->cur_vtotal = vrr->base_vtotal;
+ }
+
+ vrr->cur_err += vrr->per_frame_err;
+ } else {
vrr->cur_vtotal = vrr->max_vtotal;
+ }
if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
vrr->dynamic = true;
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index 9004f7c2f21e..cfd051ccbd2e 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -70,6 +70,9 @@ struct drm_crtc_vrr_state {
u32 vrr_min_d;
u32 vrr_max_n;
u32 vrr_max_d;
+ u32 denom;
+ u32 per_frame_err;
+ int cur_err;
u16 base_vtotal;
bool dynamic;
};
diff --git a/include/drm/drm_crtc_helper.h b/include/drm/drm_crtc_helper.h
index 0c8c11ea36dd..7d7b75045bd9 100644
--- a/include/drm/drm_crtc_helper.h
+++ b/include/drm/drm_crtc_helper.h
@@ -45,6 +45,7 @@ struct drm_framebuffer;
struct drm_mode_set;
struct drm_modeset_acquire_ctx;
struct drm_crtc_state;
+struct drm_crtc_vrr_state;
void drm_helper_disable_unused_functions(struct drm_device *dev);
int drm_crtc_helper_set_config(struct drm_mode_set *set,
@@ -63,6 +64,7 @@ int drm_helper_connector_dpms(struct drm_connector *connector, int mode);
void drm_helper_resume_force_mode(struct drm_device *dev);
int drm_helper_force_disable_all(struct drm_device *dev);
+bool drm_crtc_helper_vrr_is_fixed_rate(const struct drm_crtc_vrr_state *vrr);
bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state);
bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state);
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 11/27] drm: Implement VRR rate limiting
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Currently, VRR is allowed to use the entire range advertised by the
sink's EDID. Userspace may find it desirable, however, to limit this
range, or set a fixed target rate entirely. The latter is mainly useful
for multimedia playback applications, where judder can be avoided by
matching the content's frame rate precisely.
With the new VRR target frame rate properties, the necessary
infrastructure is in place to allow implementing this.
Add the necessary logic to hdmi_validate_vrr, as well as the VRR
helpers. Since not every target rate can be reached precisely, the
per-frame error is calculated, and dithered one row at a time across
subsequent frames. This means some frames are one vtotal larger than
others, so that averaged over the frames, the desired target rate is
achieved. Such dithering is explicitly permitted by HDMI, and is still
considered a constant target frame rate.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 134 +++++++++++++++++++++---
drivers/gpu/drm/drm_crtc_helper.c | 34 +++++-
include/drm/drm_crtc.h | 3 +
include/drm/drm_crtc_helper.h | 2 +
4 files changed, 156 insertions(+), 17 deletions(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index ce680eaa5047..846f7a72fddc 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -5,6 +5,7 @@
#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
+#include <drm/drm_crtc_helper.h>
#include <drm/drm_edid.h>
#include <drm/drm_modes.h>
#include <drm/drm_print.h>
@@ -1051,15 +1052,60 @@ static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
return 0;
}
+static int hdmi_validate_vrr_limits(struct drm_crtc_state *crtc_state, u16 vrr_min)
+{
+ struct drm_crtc_vrr_state *vrr_state = &crtc_state->vrr_state;
+ const struct drm_display_mode *mode = &crtc_state->mode;
+ u32 limit_d;
+ u8 round;
+ int ret;
+
+ /* No limiter, don't need to validate, but reset the error */
+ if (!drm_crtc_helper_vrr_is_fixed_rate(vrr_state)) {
+ vrr_state->per_frame_err = 0;
+ vrr_state->cur_err = 0;
+ return 0;
+ }
+
+ /* Don't allow rates equal to or below the minimum VRR rate / 1.001 */
+ if (cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ vrr_min * 1000, 1001) < 0)
+ return -EINVAL;
+
+ if (check_mul_overflow(mode->crtc_htotal, vrr_state->vrr_min_n, &limit_d))
+ return -ERANGE;
+
+ ret = hdmi_calculate_vtotal(&crtc_state->mode, vrr_state->vrr_min_n,
+ vrr_state->vrr_min_d, &vrr_state->base_vtotal,
+ &vrr_state->denom, &vrr_state->per_frame_err);
+ if (ret)
+ return ret;
+
+ /* Allow rounding up for the vtotal check below */
+ if (vrr_state->per_frame_err >= vrr_state->denom / 2)
+ round = 1;
+ else
+ round = 0;
+
+ if (vrr_state->base_vtotal + round < mode->crtc_vtotal)
+ return -EINVAL;
+
+ return 0;
+}
+
static int hdmi_validate_vrr(struct drm_connector *connector,
struct drm_atomic_commit *state)
{
struct drm_connector_state *new_conn_state =
drm_atomic_get_new_connector_state(state, connector);
+ struct drm_connector_state *old_conn_state =
+ drm_atomic_get_old_connector_state(state, connector);
struct drm_crtc_state *new_crtc_state =
drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
struct drm_crtc_vrr_state *vrr_state =
&new_crtc_state->vrr_state;
+ struct drm_crtc_vrr_state *old_vrr_state;
+ struct drm_crtc_state *old_crtc_state = NULL;
struct drm_display_info *info = &connector->display_info;
const struct drm_display_mode *mode = &new_crtc_state->mode;
struct drm_device *dev = connector->dev;
@@ -1068,10 +1114,14 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
u32 min_vfreq_d = 1;
u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
u32 max_vfreq_d = 1;
+ bool is_fixed;
if (!new_crtc_state->vrr_enabled)
return 0;
+ if (old_conn_state)
+ old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
+
if (!info->hdmi.vrr_cap.supported)
return -EOPNOTSUPP;
@@ -1087,6 +1137,21 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
if (!max_vfreq_n || max_vfreq_n > mode_refresh)
max_vfreq_n = mode_refresh;
+ is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
+ if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
+ min_vfreq_n = 24;
+
+ /* If the VRR target changed, make sure to reset the error accumulator */
+ if (old_crtc_state) {
+ old_vrr_state = &old_crtc_state->vrr_state;
+ if (drm_crtc_helper_vrr_is_fixed_rate(old_vrr_state) != is_fixed ||
+ cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ old_vrr_state->vrr_min_n, old_vrr_state->vrr_min_d) ||
+ cmp_fraction(vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ old_vrr_state->vrr_max_n, old_vrr_state->vrr_max_d))
+ vrr_state->cur_err = 0;
+ }
+
if (!min_vfreq_n) {
drm_dbg_kms(dev, "Could not get minimum VRR rate from sink\n");
return -EINVAL;
@@ -1098,25 +1163,64 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
return -EINVAL;
}
- if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
- drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
- max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
- return -EINVAL;
- }
-
- ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
- &vrr_state->base_vtotal, NULL, NULL);
+ ret = hdmi_validate_vrr_limits(new_crtc_state, min_vfreq_n);
if (ret) {
- drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ drm_dbg_kms(dev, "Invalid VRR fixed target rate: %pe\n", ERR_PTR(ret));
return ret;
}
- /*
- * Even at its fastest, it can't go faster than the mode, so
- * clamp to avoid imprecisely rounded mode_refresh values we
- * can't do anything about from ruining our day.
- */
- vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
+ if (!is_fixed) {
+ vrr_state->dynamic = true;
+
+ if (vrr_state->vrr_min_n) {
+ if (!vrr_state->vrr_min_d)
+ return -EINVAL;
+
+ if (cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ min_vfreq_n * 1000, 1001) < 0) {
+ drm_dbg_kms(dev, "Target min (%u/%u) < supported min (%u/%u)\n",
+ vrr_state->vrr_min_n, vrr_state->vrr_min_d,
+ min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+ min_vfreq_n = vrr_state->vrr_min_n;
+ min_vfreq_d = vrr_state->vrr_min_d;
+ }
+ if (vrr_state->vrr_max_n) {
+ if (!vrr_state->vrr_max_d)
+ return -EINVAL;
+
+ if (cmp_fraction(vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > supported max (%u/%u)\n",
+ vrr_state->vrr_max_n, vrr_state->vrr_max_d,
+ max_vfreq_n, max_vfreq_d);
+ return -EINVAL;
+ }
+ max_vfreq_n = vrr_state->vrr_max_n;
+ max_vfreq_d = vrr_state->vrr_max_d;
+ }
+ if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
+ drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
+ max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
+ return -EINVAL;
+ }
+
+ ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
+ &vrr_state->base_vtotal, NULL, NULL);
+ if (ret) {
+ drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ /*
+ * Even at its fastest, it can't go faster than the mode, so
+ * clamp to avoid imprecisely rounded mode_refresh values we
+ * can't do anything about from ruining our day.
+ */
+ vrr_state->base_vtotal = max(vrr_state->base_vtotal,
+ mode->crtc_vtotal);
+ }
if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
/* refresh <= VRR min, don't do VRR vtotal adjustment */
diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
index 4ffb3b0b7878..dd696f0c5016 100644
--- a/drivers/gpu/drm/drm_crtc_helper.c
+++ b/drivers/gpu/drm/drm_crtc_helper.c
@@ -838,6 +838,22 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
}
EXPORT_SYMBOL(drm_crtc_helper_set_config);
+bool drm_crtc_helper_vrr_is_fixed_rate(const struct drm_crtc_vrr_state *vrr)
+{
+ if (!vrr->vrr_min_n)
+ return false;
+
+ if (!vrr->vrr_min_d || !vrr->vrr_max_d)
+ return false;
+
+ if (mul_u32_u32(vrr->vrr_min_n, vrr->vrr_max_d) ==
+ mul_u32_u32(vrr->vrr_min_d, vrr->vrr_max_n))
+ return true;
+
+ return false;
+}
+EXPORT_SYMBOL(drm_crtc_helper_vrr_is_fixed_rate);
+
/**
* drm_crtc_helper_vrr_flush - Update VRR state on frame ready
* @crtc_state: pointer to &struct drm_crtc_state of CRTC that has a new frame ready
@@ -864,6 +880,9 @@ bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state)
return false;
}
+ if (drm_crtc_helper_vrr_is_fixed_rate(vrr))
+ return false;
+
if (vrr->cur_vtotal == vrr->base_vtotal)
return false;
@@ -889,10 +908,21 @@ bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
u16 old_vtotal = vrr->cur_vtotal;
- if (!crtc_state->vrr_enabled)
+ if (!crtc_state->vrr_enabled) {
vrr->cur_vtotal = 0;
- else
+ vrr->cur_err = 0;
+ } else if (drm_crtc_helper_vrr_is_fixed_rate(vrr)) {
+ if (vrr->cur_err >= (int) (vrr->denom / 2)) {
+ vrr->cur_err -= vrr->denom;
+ vrr->cur_vtotal = vrr->base_vtotal + 1;
+ } else {
+ vrr->cur_vtotal = vrr->base_vtotal;
+ }
+
+ vrr->cur_err += vrr->per_frame_err;
+ } else {
vrr->cur_vtotal = vrr->max_vtotal;
+ }
if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
vrr->dynamic = true;
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index 9004f7c2f21e..cfd051ccbd2e 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -70,6 +70,9 @@ struct drm_crtc_vrr_state {
u32 vrr_min_d;
u32 vrr_max_n;
u32 vrr_max_d;
+ u32 denom;
+ u32 per_frame_err;
+ int cur_err;
u16 base_vtotal;
bool dynamic;
};
diff --git a/include/drm/drm_crtc_helper.h b/include/drm/drm_crtc_helper.h
index 0c8c11ea36dd..7d7b75045bd9 100644
--- a/include/drm/drm_crtc_helper.h
+++ b/include/drm/drm_crtc_helper.h
@@ -45,6 +45,7 @@ struct drm_framebuffer;
struct drm_mode_set;
struct drm_modeset_acquire_ctx;
struct drm_crtc_state;
+struct drm_crtc_vrr_state;
void drm_helper_disable_unused_functions(struct drm_device *dev);
int drm_crtc_helper_set_config(struct drm_mode_set *set,
@@ -63,6 +64,7 @@ int drm_helper_connector_dpms(struct drm_connector *connector, int mode);
void drm_helper_resume_force_mode(struct drm_device *dev);
int drm_helper_force_disable_all(struct drm_device *dev);
+bool drm_crtc_helper_vrr_is_fixed_rate(const struct drm_crtc_vrr_state *vrr);
bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state);
bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state);
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 12/27] drm/rockchip: dw_hdmi_qp: Add VRR support
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Register the vrr_capable property on the connector for RK3588, where VRR
is implemented, as well as the other VRR connector properties.
In the encoder's atomic_check callback, the vrr_capable property, if
present, is then set to true if the sink supports VRR. The display limit
properties are also updated accordingly there, using the helper.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 30 ++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
index f2a1ea8dfefa..dcde50bca36b 100644
--- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
+++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
@@ -89,6 +89,9 @@
#define RK_COLOR_FMT_YUV444 0x2
#define RK_COLOR_FMT_YUV420 0x3
+#define RK_VRR_DISPLAY_MODES (BIT(VRR_DISPLAY_MODE_GAME) | \
+ BIT(VRR_DISPLAY_MODE_OFF))
+
struct rockchip_hdmi_qp {
struct device *dev;
struct regmap *regmap;
@@ -195,6 +198,7 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
+ struct drm_connector *connector = conn_state->connector;
struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state);
struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder);
union phy_configure_opts phy_cfg = {};
@@ -205,6 +209,12 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
if (ingest_fmt < 0)
return -EINVAL;
+ if (connector->vrr_capable_property) {
+ drm_connector_set_vrr_capable_property(connector,
+ drm_display_info_is_vrr_capable(&connector->display_info));
+ drm_connector_hdmi_update_vrr_limits(connector, &crtc_state->mode);
+ }
+
if (hdmi->tmds_char_rate == conn_state->hdmi.tmds_char_rate &&
s->output_bpc == conn_state->hdmi.output_bpc &&
s->bus_format == ingest_fmt)
@@ -552,6 +562,7 @@ struct rockchip_hdmi_qp_cfg {
unsigned int port_ids[MAX_HDMI_PORT_NUM];
const struct rockchip_hdmi_qp_ctrl_ops *ctrl_ops;
const struct dw_hdmi_qp_phy_ops *phy_ops;
+ bool supports_vrr;
};
static const struct rockchip_hdmi_qp_cfg rk3576_hdmi_cfg = {
@@ -571,6 +582,7 @@ static const struct rockchip_hdmi_qp_cfg rk3588_hdmi_cfg = {
},
.ctrl_ops = &rk3588_hdmi_ctrl_ops,
.phy_ops = &rk3588_hdmi_phy_ops,
+ .supports_vrr = true,
};
static const struct of_device_id dw_hdmi_qp_rockchip_dt_ids[] = {
@@ -729,6 +741,24 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master,
return dev_err_probe(dev, PTR_ERR(connector),
"Failed to init bridge connector\n");
+ if (cfg->supports_vrr) {
+ ret = drm_connector_attach_vrr_capable_property(connector);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach 'vrr_capable' property\n");
+
+ ret = drm_connector_attach_vrr_display_mode_property(connector,
+ RK_VRR_DISPLAY_MODES);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach 'vrr_display_mode' property\n");
+
+ ret = drm_connector_attach_vrr_display_limits_properties(connector);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach VRR display limits properties\n");
+ }
+
platform_set_drvdata(pdev, hdmi);
return 0;
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 12/27] drm/rockchip: dw_hdmi_qp: Add VRR support
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Register the vrr_capable property on the connector for RK3588, where VRR
is implemented, as well as the other VRR connector properties.
In the encoder's atomic_check callback, the vrr_capable property, if
present, is then set to true if the sink supports VRR. The display limit
properties are also updated accordingly there, using the helper.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 30 ++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
index f2a1ea8dfefa..dcde50bca36b 100644
--- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
+++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
@@ -89,6 +89,9 @@
#define RK_COLOR_FMT_YUV444 0x2
#define RK_COLOR_FMT_YUV420 0x3
+#define RK_VRR_DISPLAY_MODES (BIT(VRR_DISPLAY_MODE_GAME) | \
+ BIT(VRR_DISPLAY_MODE_OFF))
+
struct rockchip_hdmi_qp {
struct device *dev;
struct regmap *regmap;
@@ -195,6 +198,7 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
+ struct drm_connector *connector = conn_state->connector;
struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state);
struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder);
union phy_configure_opts phy_cfg = {};
@@ -205,6 +209,12 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
if (ingest_fmt < 0)
return -EINVAL;
+ if (connector->vrr_capable_property) {
+ drm_connector_set_vrr_capable_property(connector,
+ drm_display_info_is_vrr_capable(&connector->display_info));
+ drm_connector_hdmi_update_vrr_limits(connector, &crtc_state->mode);
+ }
+
if (hdmi->tmds_char_rate == conn_state->hdmi.tmds_char_rate &&
s->output_bpc == conn_state->hdmi.output_bpc &&
s->bus_format == ingest_fmt)
@@ -552,6 +562,7 @@ struct rockchip_hdmi_qp_cfg {
unsigned int port_ids[MAX_HDMI_PORT_NUM];
const struct rockchip_hdmi_qp_ctrl_ops *ctrl_ops;
const struct dw_hdmi_qp_phy_ops *phy_ops;
+ bool supports_vrr;
};
static const struct rockchip_hdmi_qp_cfg rk3576_hdmi_cfg = {
@@ -571,6 +582,7 @@ static const struct rockchip_hdmi_qp_cfg rk3588_hdmi_cfg = {
},
.ctrl_ops = &rk3588_hdmi_ctrl_ops,
.phy_ops = &rk3588_hdmi_phy_ops,
+ .supports_vrr = true,
};
static const struct of_device_id dw_hdmi_qp_rockchip_dt_ids[] = {
@@ -729,6 +741,24 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master,
return dev_err_probe(dev, PTR_ERR(connector),
"Failed to init bridge connector\n");
+ if (cfg->supports_vrr) {
+ ret = drm_connector_attach_vrr_capable_property(connector);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach 'vrr_capable' property\n");
+
+ ret = drm_connector_attach_vrr_display_mode_property(connector,
+ RK_VRR_DISPLAY_MODES);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach 'vrr_display_mode' property\n");
+
+ ret = drm_connector_attach_vrr_display_limits_properties(connector);
+ if (ret)
+ return dev_err_probe(hdmi->dev, ret,
+ "Failed to attach VRR display limits properties\n");
+ }
+
platform_set_drvdata(pdev, hdmi);
return 0;
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 13/27] drm/rockchip: Set mode_changed on vrr_enabled change
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
RK3588 (and likely other Rockchip hardware) requires a full modeset when
VRR is enabled in order for it to function properly.
Set mode_changed during the atomic check phase to do this.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_fb.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_fb.c b/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
index 45588b79b3d4..e3aad75b88b4 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
@@ -13,6 +13,7 @@
#include <drm/drm_framebuffer.h>
#include <drm/drm_gem_framebuffer_helper.h>
#include <drm/drm_probe_helper.h>
+#include <drm/drm_print.h>
#include "rockchip_drm_drv.h"
#include "rockchip_drm_fb.h"
@@ -59,9 +60,26 @@ rockchip_fb_create(struct drm_device *dev, struct drm_file *file,
return &afbc_fb->base;
}
+static int rockchip_drm_atomic_check(struct drm_device *dev, struct drm_atomic_commit *state)
+{
+ struct drm_crtc_state *old_crtc_state, *new_crtc_state;
+ struct drm_crtc *crtc;
+ int i;
+
+ for_each_oldnew_crtc_in_state(state, crtc, old_crtc_state, new_crtc_state, i) {
+ if (old_crtc_state->vrr_enabled != new_crtc_state->vrr_enabled) {
+ new_crtc_state->mode_changed = true;
+ drm_dbg_driver(dev, "[CRTC:%d:%s] VRR changed\n",
+ crtc->base.id, crtc->name);
+ }
+ }
+
+ return drm_atomic_helper_check(dev, state);
+}
+
static const struct drm_mode_config_funcs rockchip_drm_mode_config_funcs = {
.fb_create = rockchip_fb_create,
- .atomic_check = drm_atomic_helper_check,
+ .atomic_check = rockchip_drm_atomic_check,
.atomic_commit = drm_atomic_helper_commit,
};
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 13/27] drm/rockchip: Set mode_changed on vrr_enabled change
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
RK3588 (and likely other Rockchip hardware) requires a full modeset when
VRR is enabled in order for it to function properly.
Set mode_changed during the atomic check phase to do this.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_fb.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_fb.c b/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
index 45588b79b3d4..e3aad75b88b4 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_fb.c
@@ -13,6 +13,7 @@
#include <drm/drm_framebuffer.h>
#include <drm/drm_gem_framebuffer_helper.h>
#include <drm/drm_probe_helper.h>
+#include <drm/drm_print.h>
#include "rockchip_drm_drv.h"
#include "rockchip_drm_fb.h"
@@ -59,9 +60,26 @@ rockchip_fb_create(struct drm_device *dev, struct drm_file *file,
return &afbc_fb->base;
}
+static int rockchip_drm_atomic_check(struct drm_device *dev, struct drm_atomic_commit *state)
+{
+ struct drm_crtc_state *old_crtc_state, *new_crtc_state;
+ struct drm_crtc *crtc;
+ int i;
+
+ for_each_oldnew_crtc_in_state(state, crtc, old_crtc_state, new_crtc_state, i) {
+ if (old_crtc_state->vrr_enabled != new_crtc_state->vrr_enabled) {
+ new_crtc_state->mode_changed = true;
+ drm_dbg_driver(dev, "[CRTC:%d:%s] VRR changed\n",
+ crtc->base.id, crtc->name);
+ }
+ }
+
+ return drm_atomic_helper_check(dev, state);
+}
+
static const struct drm_mode_config_funcs rockchip_drm_mode_config_funcs = {
.fb_create = rockchip_fb_create,
- .atomic_check = drm_atomic_helper_check,
+ .atomic_check = rockchip_drm_atomic_check,
.atomic_commit = drm_atomic_helper_commit,
};
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 14/27] drm/rockchip: vop2: Enable VRR
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Use the new VRR state tracking to handle VRR front porch adjustment.
To avoid indenting too deeply, refactor the vblank handling into its own
function while at it, where scope-based cleanup helpers for the lock can
be used.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 65 +++++++++++++++++++++++-----
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 10 +++++
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 4 ++
3 files changed, 67 insertions(+), 12 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index bfb384142568..d93b49025795 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -24,6 +24,7 @@
#include <drm/drm_atomic_uapi.h>
#include <drm/drm_blend.h>
#include <drm/drm_crtc.h>
+#include <drm/drm_crtc_helper.h>
#include <linux/debugfs.h>
#include <drm/drm_debugfs.h>
#include <drm/drm_flip_work.h>
@@ -2011,6 +2012,25 @@ static void vop2_crtc_atomic_begin(struct drm_crtc *crtc,
vop2->ops->setup_overlay(vp);
}
+static void vop2_vp_set_vrr_vtotal(struct vop2_video_port *vp, unsigned int vtotal)
+{
+ const u32 mask = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL |
+ RK3568_VP_DSP_VTOTAL_VS_END__IMD;
+
+ if (!vtotal) {
+ vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END,
+ RK3568_VP_DSP_VTOTAL_VS_END__IMD, 0);
+ return;
+ }
+
+ /* Value is 13 bits at most, saturate it if too large */
+ if (vtotal > RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16)
+ vtotal = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16;
+
+ vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END, mask,
+ vtotal << 16 | RK3568_VP_DSP_VTOTAL_VS_END__IMD);
+}
+
static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
struct drm_atomic_commit *state)
{
@@ -2029,6 +2049,11 @@ static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
spin_lock_irq(&crtc->dev->event_lock);
+ if (crtc_state->vrr_enabled) {
+ if (drm_crtc_helper_vrr_flush(crtc_state))
+ vop2_vp_set_vrr_vtotal(vp, crtc_state->vrr_state.cur_vtotal);
+ }
+
if (crtc->state->event) {
WARN_ON(drm_crtc_vblank_get(crtc));
vp->event = crtc->state->event;
@@ -2371,6 +2396,30 @@ static irqreturn_t rk3576_vp_isr(int irq, void *data)
return ret;
}
+static void vop2_vp_handle_vblank(struct vop2_video_port *vp)
+{
+ struct drm_crtc *crtc = &vp->crtc;
+ struct vop2 *vop2 = vp->vop2;
+ u32 val;
+
+ guard(spinlock)(&crtc->dev->event_lock);
+
+ if (!vp->event)
+ return;
+
+ val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
+
+ if (!(val & BIT(vp->id))) {
+ drm_crtc_send_vblank_event(crtc, vp->event);
+ vp->event = NULL;
+ drm_crtc_vblank_put(crtc);
+
+ if (crtc->state->vrr_enabled)
+ if (drm_crtc_helper_vrr_vsync(crtc->state))
+ vop2_vp_set_vrr_vtotal(vp, crtc->state->vrr_state.cur_vtotal);
+ }
+}
+
static irqreturn_t vop2_isr(int irq, void *data)
{
struct vop2 *vop2 = data;
@@ -2402,18 +2451,7 @@ static irqreturn_t vop2_isr(int irq, void *data)
if (irqs & VP_INT_FS_FIELD) {
drm_crtc_handle_vblank(crtc);
- spin_lock(&crtc->dev->event_lock);
- if (vp->event) {
- u32 val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
-
- if (!(val & BIT(vp->id))) {
- drm_crtc_send_vblank_event(crtc, vp->event);
- vp->event = NULL;
- drm_crtc_vblank_put(crtc);
- }
- }
- spin_unlock(&crtc->dev->event_lock);
-
+ vop2_vp_handle_vblank(vp);
ret = IRQ_HANDLED;
}
@@ -2635,6 +2673,9 @@ static int vop2_create_crtcs(struct vop2 *vop2)
drm_crtc_attach_background_color_property(&vp->crtc);
+ if (vp->data->vrr_limiter_capable)
+ drm_crtc_attach_vrr_limiter_properties(&vp->crtc);
+
drm_crtc_helper_add(&vp->crtc, &vop2_crtc_helper_funcs);
if (vop2->lut_regs) {
const struct vop2_video_port_data *vp_data = &vop2_data->vp[vp->id];
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
index ffcb39c130aa..323e2533de34 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
@@ -231,6 +231,7 @@ struct vop2_video_port_data {
* @pixel_rate: pixel per cycle
*/
u8 pixel_rate;
+ bool vrr_limiter_capable;
};
struct vop2_video_port {
@@ -655,6 +656,9 @@ enum dst_factor_mode {
#define RK3588_VP_DSP_CTRL__GAMMA_UPDATE_EN BIT(22)
+#define RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL GENMASK(28, 16)
+#define RK3568_VP_DSP_VTOTAL_VS_END__IMD BIT(15)
+
#define RK3588_VP_CLK_CTRL__DCLK_OUT_DIV GENMASK(3, 2)
#define RK3588_VP_CLK_CTRL__DCLK_CORE_DIV GENMASK(1, 0)
@@ -810,6 +814,12 @@ static inline void vop2_vp_write(struct vop2_video_port *vp, u32 offset, u32 v)
regmap_write(vp->vop2->map, vp->data->offset + offset, v);
}
+static inline void vop2_vp_update_bits(struct vop2_video_port *vp, u32 offset,
+ u32 mask, u32 v)
+{
+ regmap_update_bits(vp->vop2->map, vp->data->offset + offset, mask, v);
+}
+
static inline u32 vop2_readl(struct vop2 *vop2, u32 offset)
{
u32 val;
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index c44648e1bc0d..946ca420f3f5 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -1063,6 +1063,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
/* hdr2sdr sdr2hdr hdr2hdr sdr2sdr */
.pre_scan_max_dly = { 76, 65, 65, 54 },
.offset = 0xc00,
+ .vrr_limiter_capable = true,
}, {
.id = 1,
.feature = VOP2_VP_FEATURE_OUTPUT_10BIT,
@@ -1071,6 +1072,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
.max_output = { 4096, 2304 },
.pre_scan_max_dly = { 76, 65, 65, 54 },
.offset = 0xd00,
+ .vrr_limiter_capable = true,
}, {
.id = 2,
.feature = VOP2_VP_FEATURE_OUTPUT_10BIT,
@@ -1079,12 +1081,14 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
.max_output = { 4096, 2304 },
.pre_scan_max_dly = { 52, 52, 52, 52 },
.offset = 0xe00,
+ .vrr_limiter_capable = true,
}, {
.id = 3,
.gamma_lut_len = 1024,
.max_output = { 2048, 1536 },
.pre_scan_max_dly = { 52, 52, 52, 52 },
.offset = 0xf00,
+ .vrr_limiter_capable = true,
},
};
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 14/27] drm/rockchip: vop2: Enable VRR
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Use the new VRR state tracking to handle VRR front porch adjustment.
To avoid indenting too deeply, refactor the vblank handling into its own
function while at it, where scope-based cleanup helpers for the lock can
be used.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 65 +++++++++++++++++++++++-----
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 10 +++++
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 4 ++
3 files changed, 67 insertions(+), 12 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index bfb384142568..d93b49025795 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -24,6 +24,7 @@
#include <drm/drm_atomic_uapi.h>
#include <drm/drm_blend.h>
#include <drm/drm_crtc.h>
+#include <drm/drm_crtc_helper.h>
#include <linux/debugfs.h>
#include <drm/drm_debugfs.h>
#include <drm/drm_flip_work.h>
@@ -2011,6 +2012,25 @@ static void vop2_crtc_atomic_begin(struct drm_crtc *crtc,
vop2->ops->setup_overlay(vp);
}
+static void vop2_vp_set_vrr_vtotal(struct vop2_video_port *vp, unsigned int vtotal)
+{
+ const u32 mask = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL |
+ RK3568_VP_DSP_VTOTAL_VS_END__IMD;
+
+ if (!vtotal) {
+ vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END,
+ RK3568_VP_DSP_VTOTAL_VS_END__IMD, 0);
+ return;
+ }
+
+ /* Value is 13 bits at most, saturate it if too large */
+ if (vtotal > RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16)
+ vtotal = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16;
+
+ vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END, mask,
+ vtotal << 16 | RK3568_VP_DSP_VTOTAL_VS_END__IMD);
+}
+
static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
struct drm_atomic_commit *state)
{
@@ -2029,6 +2049,11 @@ static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
spin_lock_irq(&crtc->dev->event_lock);
+ if (crtc_state->vrr_enabled) {
+ if (drm_crtc_helper_vrr_flush(crtc_state))
+ vop2_vp_set_vrr_vtotal(vp, crtc_state->vrr_state.cur_vtotal);
+ }
+
if (crtc->state->event) {
WARN_ON(drm_crtc_vblank_get(crtc));
vp->event = crtc->state->event;
@@ -2371,6 +2396,30 @@ static irqreturn_t rk3576_vp_isr(int irq, void *data)
return ret;
}
+static void vop2_vp_handle_vblank(struct vop2_video_port *vp)
+{
+ struct drm_crtc *crtc = &vp->crtc;
+ struct vop2 *vop2 = vp->vop2;
+ u32 val;
+
+ guard(spinlock)(&crtc->dev->event_lock);
+
+ if (!vp->event)
+ return;
+
+ val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
+
+ if (!(val & BIT(vp->id))) {
+ drm_crtc_send_vblank_event(crtc, vp->event);
+ vp->event = NULL;
+ drm_crtc_vblank_put(crtc);
+
+ if (crtc->state->vrr_enabled)
+ if (drm_crtc_helper_vrr_vsync(crtc->state))
+ vop2_vp_set_vrr_vtotal(vp, crtc->state->vrr_state.cur_vtotal);
+ }
+}
+
static irqreturn_t vop2_isr(int irq, void *data)
{
struct vop2 *vop2 = data;
@@ -2402,18 +2451,7 @@ static irqreturn_t vop2_isr(int irq, void *data)
if (irqs & VP_INT_FS_FIELD) {
drm_crtc_handle_vblank(crtc);
- spin_lock(&crtc->dev->event_lock);
- if (vp->event) {
- u32 val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
-
- if (!(val & BIT(vp->id))) {
- drm_crtc_send_vblank_event(crtc, vp->event);
- vp->event = NULL;
- drm_crtc_vblank_put(crtc);
- }
- }
- spin_unlock(&crtc->dev->event_lock);
-
+ vop2_vp_handle_vblank(vp);
ret = IRQ_HANDLED;
}
@@ -2635,6 +2673,9 @@ static int vop2_create_crtcs(struct vop2 *vop2)
drm_crtc_attach_background_color_property(&vp->crtc);
+ if (vp->data->vrr_limiter_capable)
+ drm_crtc_attach_vrr_limiter_properties(&vp->crtc);
+
drm_crtc_helper_add(&vp->crtc, &vop2_crtc_helper_funcs);
if (vop2->lut_regs) {
const struct vop2_video_port_data *vp_data = &vop2_data->vp[vp->id];
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
index ffcb39c130aa..323e2533de34 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
@@ -231,6 +231,7 @@ struct vop2_video_port_data {
* @pixel_rate: pixel per cycle
*/
u8 pixel_rate;
+ bool vrr_limiter_capable;
};
struct vop2_video_port {
@@ -655,6 +656,9 @@ enum dst_factor_mode {
#define RK3588_VP_DSP_CTRL__GAMMA_UPDATE_EN BIT(22)
+#define RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL GENMASK(28, 16)
+#define RK3568_VP_DSP_VTOTAL_VS_END__IMD BIT(15)
+
#define RK3588_VP_CLK_CTRL__DCLK_OUT_DIV GENMASK(3, 2)
#define RK3588_VP_CLK_CTRL__DCLK_CORE_DIV GENMASK(1, 0)
@@ -810,6 +814,12 @@ static inline void vop2_vp_write(struct vop2_video_port *vp, u32 offset, u32 v)
regmap_write(vp->vop2->map, vp->data->offset + offset, v);
}
+static inline void vop2_vp_update_bits(struct vop2_video_port *vp, u32 offset,
+ u32 mask, u32 v)
+{
+ regmap_update_bits(vp->vop2->map, vp->data->offset + offset, mask, v);
+}
+
static inline u32 vop2_readl(struct vop2 *vop2, u32 offset)
{
u32 val;
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index c44648e1bc0d..946ca420f3f5 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -1063,6 +1063,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
/* hdr2sdr sdr2hdr hdr2hdr sdr2sdr */
.pre_scan_max_dly = { 76, 65, 65, 54 },
.offset = 0xc00,
+ .vrr_limiter_capable = true,
}, {
.id = 1,
.feature = VOP2_VP_FEATURE_OUTPUT_10BIT,
@@ -1071,6 +1072,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
.max_output = { 4096, 2304 },
.pre_scan_max_dly = { 76, 65, 65, 54 },
.offset = 0xd00,
+ .vrr_limiter_capable = true,
}, {
.id = 2,
.feature = VOP2_VP_FEATURE_OUTPUT_10BIT,
@@ -1079,12 +1081,14 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = {
.max_output = { 4096, 2304 },
.pre_scan_max_dly = { 52, 52, 52, 52 },
.offset = 0xe00,
+ .vrr_limiter_capable = true,
}, {
.id = 3,
.gamma_lut_len = 1024,
.max_output = { 2048, 1536 },
.pre_scan_max_dly = { 52, 52, 52, 52 },
.offset = 0xf00,
+ .vrr_limiter_capable = true,
},
};
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 15/27] drm/edid: Parse QMS flag from HDMI SCDS
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Thanks to a GPL'd source code release from a hardware vendor that shall
not be named, which has since been scrubbed off GitHub, it's now public
knowledge how the HDMI Forum EDID extension communicates support for QMS
("Quick Media Switching") on a sink. Bit number 6 indicates support for
QMS in the same byte that also contains the CinemaVRR flag.
Add this definition to the EDID header file, and parse it to a member of
drm_hdmi_vrr_cap.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_edid.c | 1 +
include/drm/drm_connector.h | 3 +++
include/drm/drm_edid.h | 1 +
3 files changed, 5 insertions(+)
diff --git a/drivers/gpu/drm/drm_edid.c b/drivers/gpu/drm/drm_edid.c
index 3148038747b6..d439928caacc 100644
--- a/drivers/gpu/drm/drm_edid.c
+++ b/drivers/gpu/drm/drm_edid.c
@@ -6224,6 +6224,7 @@ static void drm_parse_hdmi_gaming_info(struct drm_hdmi_info *hdmi, const u8 *db)
vrr->cnmvrr = db[8] & DRM_EDID_CNMVRR;
vrr->cinema_vrr = db[8] & DRM_EDID_CINEMA_VRR;
vrr->mdelta = db[8] & DRM_EDID_MDELTA;
+ vrr->qms_capable = db[8] & DRM_EDID_QMS;
if (cea_db_payload_len(db) < 9)
return;
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index c0865a2547fd..830a33fb5301 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -291,6 +291,9 @@ struct drm_hdmi_vrr_cap {
* VRRmax values having correct values.
*/
bool supported;
+
+ /** @qms_capable: The sink supports Quick Media Switching */
+ bool qms_capable;
};
/**
diff --git a/include/drm/drm_edid.h b/include/drm/drm_edid.h
index a2617aa34edf..f1e2052cb08b 100644
--- a/include/drm/drm_edid.h
+++ b/include/drm/drm_edid.h
@@ -265,6 +265,7 @@ struct detailed_timing {
#define DRM_EDID_CNMVRR (1 << 3)
#define DRM_EDID_CINEMA_VRR (1 << 4)
#define DRM_EDID_MDELTA (1 << 5)
+#define DRM_EDID_QMS (1 << 6)
#define DRM_EDID_VRR_MAX_UPPER_MASK 0xc0
#define DRM_EDID_VRR_MAX_LOWER_MASK 0xff
#define DRM_EDID_VRR_MIN_MASK 0x3f
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 15/27] drm/edid: Parse QMS flag from HDMI SCDS
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Thanks to a GPL'd source code release from a hardware vendor that shall
not be named, which has since been scrubbed off GitHub, it's now public
knowledge how the HDMI Forum EDID extension communicates support for QMS
("Quick Media Switching") on a sink. Bit number 6 indicates support for
QMS in the same byte that also contains the CinemaVRR flag.
Add this definition to the EDID header file, and parse it to a member of
drm_hdmi_vrr_cap.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_edid.c | 1 +
include/drm/drm_connector.h | 3 +++
include/drm/drm_edid.h | 1 +
3 files changed, 5 insertions(+)
diff --git a/drivers/gpu/drm/drm_edid.c b/drivers/gpu/drm/drm_edid.c
index 3148038747b6..d439928caacc 100644
--- a/drivers/gpu/drm/drm_edid.c
+++ b/drivers/gpu/drm/drm_edid.c
@@ -6224,6 +6224,7 @@ static void drm_parse_hdmi_gaming_info(struct drm_hdmi_info *hdmi, const u8 *db)
vrr->cnmvrr = db[8] & DRM_EDID_CNMVRR;
vrr->cinema_vrr = db[8] & DRM_EDID_CINEMA_VRR;
vrr->mdelta = db[8] & DRM_EDID_MDELTA;
+ vrr->qms_capable = db[8] & DRM_EDID_QMS;
if (cea_db_payload_len(db) < 9)
return;
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index c0865a2547fd..830a33fb5301 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -291,6 +291,9 @@ struct drm_hdmi_vrr_cap {
* VRRmax values having correct values.
*/
bool supported;
+
+ /** @qms_capable: The sink supports Quick Media Switching */
+ bool qms_capable;
};
/**
diff --git a/include/drm/drm_edid.h b/include/drm/drm_edid.h
index a2617aa34edf..f1e2052cb08b 100644
--- a/include/drm/drm_edid.h
+++ b/include/drm/drm_edid.h
@@ -265,6 +265,7 @@ struct detailed_timing {
#define DRM_EDID_CNMVRR (1 << 3)
#define DRM_EDID_CINEMA_VRR (1 << 4)
#define DRM_EDID_MDELTA (1 << 5)
+#define DRM_EDID_QMS (1 << 6)
#define DRM_EDID_VRR_MAX_UPPER_MASK 0xc0
#define DRM_EDID_VRR_MAX_LOWER_MASK 0xff
#define DRM_EDID_VRR_MIN_MASK 0x3f
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 16/27] drm/edid: Parse QMS TFR min/max flags from HDMI SCDS
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
HDMI has two flags in the SCDS EDID extension that signal which minimum
and maximum refresh rate the sink wishes to use when doing QMS ("Quick
Media Switching").
How to parse these flags, and what they do, has been learned from the
publicly available libdisplay-info source code.
Add the definitions of where in the byte they are, the code for checking
for them, as well as two new booleans in drm_hdmi_vrr_cap to store the
result.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_edid.c | 6 ++++++
include/drm/drm_connector.h | 12 ++++++++++++
include/drm/drm_edid.h | 2 ++
3 files changed, 20 insertions(+)
diff --git a/drivers/gpu/drm/drm_edid.c b/drivers/gpu/drm/drm_edid.c
index d439928caacc..39b5c01309e2 100644
--- a/drivers/gpu/drm/drm_edid.c
+++ b/drivers/gpu/drm/drm_edid.c
@@ -6237,6 +6237,12 @@ static void drm_parse_hdmi_gaming_info(struct drm_hdmi_info *hdmi, const u8 *db)
vrr->vrr_max = (db[9] & DRM_EDID_VRR_MAX_UPPER_MASK) << 2 | db[10];
vrr->supported &= (vrr->vrr_max == 0 || vrr->vrr_max >= 100);
+
+ if (cea_db_payload_len(db) < 11)
+ return;
+
+ vrr->qms_tfr_min = db[11] & DRM_EDID_QMS_TFR_MIN;
+ vrr->qms_tfr_max = db[11] & DRM_EDID_QMS_TFR_MAX;
}
static void drm_parse_dsc_info(struct drm_hdmi_dsc_cap *hdmi_dsc,
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 830a33fb5301..4801bb2769e1 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -294,6 +294,18 @@ struct drm_hdmi_vrr_cap {
/** @qms_capable: The sink supports Quick Media Switching */
bool qms_capable;
+
+ /**
+ * @qms_tfr_min: If set, the sink uses 24/1.001Hz as its minimum rate
+ * for QMS. If unset, @drm_display_info.monitor_range.min_vfreq is used.
+ */
+ bool qms_tfr_min;
+
+ /**
+ * @qms_tfr_max: If set, @drm_display_info.monitor_range.max_vfreq is
+ * used by the sink as its maximum rate for QMS. If unset, 60Hz is used.
+ */
+ bool qms_tfr_max;
};
/**
diff --git a/include/drm/drm_edid.h b/include/drm/drm_edid.h
index f1e2052cb08b..d339acd9ee52 100644
--- a/include/drm/drm_edid.h
+++ b/include/drm/drm_edid.h
@@ -269,6 +269,8 @@ struct detailed_timing {
#define DRM_EDID_VRR_MAX_UPPER_MASK 0xc0
#define DRM_EDID_VRR_MAX_LOWER_MASK 0xff
#define DRM_EDID_VRR_MIN_MASK 0x3f
+#define DRM_EDID_QMS_TFR_MIN (1 << 4)
+#define DRM_EDID_QMS_TFR_MAX (1 << 5)
/* DSC specific */
#define DRM_EDID_DSC_10BPC (1 << 0)
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 16/27] drm/edid: Parse QMS TFR min/max flags from HDMI SCDS
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
HDMI has two flags in the SCDS EDID extension that signal which minimum
and maximum refresh rate the sink wishes to use when doing QMS ("Quick
Media Switching").
How to parse these flags, and what they do, has been learned from the
publicly available libdisplay-info source code.
Add the definitions of where in the byte they are, the code for checking
for them, as well as two new booleans in drm_hdmi_vrr_cap to store the
result.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_edid.c | 6 ++++++
include/drm/drm_connector.h | 12 ++++++++++++
include/drm/drm_edid.h | 2 ++
3 files changed, 20 insertions(+)
diff --git a/drivers/gpu/drm/drm_edid.c b/drivers/gpu/drm/drm_edid.c
index d439928caacc..39b5c01309e2 100644
--- a/drivers/gpu/drm/drm_edid.c
+++ b/drivers/gpu/drm/drm_edid.c
@@ -6237,6 +6237,12 @@ static void drm_parse_hdmi_gaming_info(struct drm_hdmi_info *hdmi, const u8 *db)
vrr->vrr_max = (db[9] & DRM_EDID_VRR_MAX_UPPER_MASK) << 2 | db[10];
vrr->supported &= (vrr->vrr_max == 0 || vrr->vrr_max >= 100);
+
+ if (cea_db_payload_len(db) < 11)
+ return;
+
+ vrr->qms_tfr_min = db[11] & DRM_EDID_QMS_TFR_MIN;
+ vrr->qms_tfr_max = db[11] & DRM_EDID_QMS_TFR_MAX;
}
static void drm_parse_dsc_info(struct drm_hdmi_dsc_cap *hdmi_dsc,
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 830a33fb5301..4801bb2769e1 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -294,6 +294,18 @@ struct drm_hdmi_vrr_cap {
/** @qms_capable: The sink supports Quick Media Switching */
bool qms_capable;
+
+ /**
+ * @qms_tfr_min: If set, the sink uses 24/1.001Hz as its minimum rate
+ * for QMS. If unset, @drm_display_info.monitor_range.min_vfreq is used.
+ */
+ bool qms_tfr_min;
+
+ /**
+ * @qms_tfr_max: If set, @drm_display_info.monitor_range.max_vfreq is
+ * used by the sink as its maximum rate for QMS. If unset, 60Hz is used.
+ */
+ bool qms_tfr_max;
};
/**
diff --git a/include/drm/drm_edid.h b/include/drm/drm_edid.h
index f1e2052cb08b..d339acd9ee52 100644
--- a/include/drm/drm_edid.h
+++ b/include/drm/drm_edid.h
@@ -269,6 +269,8 @@ struct detailed_timing {
#define DRM_EDID_VRR_MAX_UPPER_MASK 0xc0
#define DRM_EDID_VRR_MAX_LOWER_MASK 0xff
#define DRM_EDID_VRR_MIN_MASK 0x3f
+#define DRM_EDID_QMS_TFR_MIN (1 << 4)
+#define DRM_EDID_QMS_TFR_MAX (1 << 5)
/* DSC specific */
#define DRM_EDID_DSC_10BPC (1 << 0)
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 17/27] drm/connector: Add "Media" VRR display mode for QMS
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
HDMI includes a feature called "Quick Media Switching", or QMS for
short, which builds upon the existing VRR to allow for quick switching
between a select few common frame rates.
Add a new VRR display mode, "VRR_DISPLAY_MODE_MEDIA", to the
vrr_display_mode connector property, in order to expose support for
this.
Additionally, add a boolean flag to the connector's HDMI sub-struct with
which source-side support for QMS can be indicated, as well as the QMS
tfr byte to the HDMI state sub-struct.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_connector.c | 6 ++++++
include/drm/drm_connector.h | 14 ++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 21222214a843..f7d619926e2d 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1456,6 +1456,7 @@ DRM_ENUM_NAME_FN(drm_get_tv_mode_name, drm_tv_mode_enum_list)
static const struct drm_prop_enum_list drm_vrr_mode_enum_list[] = {
{ VRR_DISPLAY_MODE_GAME, "Game" },
{ VRR_DISPLAY_MODE_OFF, "Off" },
+ { VRR_DISPLAY_MODE_MEDIA, "Media" },
};
DRM_ENUM_NAME_FN(drm_get_vrr_mode_name, drm_vrr_mode_enum_list)
@@ -2593,6 +2594,11 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* - Game - set display to game mode if display supports it,
* regardless of CRTC state
* - Off - set display to VRR disabled mode, regardless of CRTC state
+ * - Media - a "media" VRR mode that only supports a select number of
+ * common media refresh rates as fixed targets, but does so with
+ * possibly better display-side processing. In the HDMI display
+ * protocol, this is QMS (Quick Media Switching), though the mode
+ * enum itself is vendor-agnostic
*
* VRR Limiter/Target Properties
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 4801bb2769e1..41f272afbb1e 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -829,6 +829,8 @@ enum vrr_display_mode {
VRR_DISPLAY_MODE_GAME = 0,
/** @VRR_DISPLAY_MODE_OFF: Turn off VRR on the display */
VRR_DISPLAY_MODE_OFF,
+ /** @VRR_DISPLAY_MODE_MEDIA: Protocol-agnostic fixed rate media mode */
+ VRR_DISPLAY_MODE_MEDIA,
/** @VRR_DISPLAY_MODE_COUNT: Sentinel, not a valid mode, keep last */
VRR_DISPLAY_MODE_COUNT
};
@@ -1241,6 +1243,12 @@ struct drm_connector_hdmi_state {
* deliberately not requested by the helper.
*/
bool scrambler_needed;
+
+ /**
+ * @qms_tfr_byte: Target frame rate byte for HDMI QMS, or 0 if QMS is
+ * not in use.
+ */
+ u8 qms_tfr_byte;
};
/**
@@ -2270,6 +2278,12 @@ struct drm_connector_hdmi {
*/
bool scrambler_enabled;
+ /**
+ * @qms_supported: Whether the connector supports HDMI Quick Media
+ * Switching (QMS) on the source side, irrespective of display support.
+ */
+ bool qms_supported;
+
/**
* @scdc_work: Work item currently used to monitor sink-side scrambling
* status and retry setup if the sink resets it.
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 17/27] drm/connector: Add "Media" VRR display mode for QMS
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
HDMI includes a feature called "Quick Media Switching", or QMS for
short, which builds upon the existing VRR to allow for quick switching
between a select few common frame rates.
Add a new VRR display mode, "VRR_DISPLAY_MODE_MEDIA", to the
vrr_display_mode connector property, in order to expose support for
this.
Additionally, add a boolean flag to the connector's HDMI sub-struct with
which source-side support for QMS can be indicated, as well as the QMS
tfr byte to the HDMI state sub-struct.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_connector.c | 6 ++++++
include/drm/drm_connector.h | 14 ++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
index 21222214a843..f7d619926e2d 100644
--- a/drivers/gpu/drm/drm_connector.c
+++ b/drivers/gpu/drm/drm_connector.c
@@ -1456,6 +1456,7 @@ DRM_ENUM_NAME_FN(drm_get_tv_mode_name, drm_tv_mode_enum_list)
static const struct drm_prop_enum_list drm_vrr_mode_enum_list[] = {
{ VRR_DISPLAY_MODE_GAME, "Game" },
{ VRR_DISPLAY_MODE_OFF, "Off" },
+ { VRR_DISPLAY_MODE_MEDIA, "Media" },
};
DRM_ENUM_NAME_FN(drm_get_vrr_mode_name, drm_vrr_mode_enum_list)
@@ -2593,6 +2594,11 @@ EXPORT_SYMBOL(drm_mode_create_scaling_mode_property);
* - Game - set display to game mode if display supports it,
* regardless of CRTC state
* - Off - set display to VRR disabled mode, regardless of CRTC state
+ * - Media - a "media" VRR mode that only supports a select number of
+ * common media refresh rates as fixed targets, but does so with
+ * possibly better display-side processing. In the HDMI display
+ * protocol, this is QMS (Quick Media Switching), though the mode
+ * enum itself is vendor-agnostic
*
* VRR Limiter/Target Properties
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 4801bb2769e1..41f272afbb1e 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -829,6 +829,8 @@ enum vrr_display_mode {
VRR_DISPLAY_MODE_GAME = 0,
/** @VRR_DISPLAY_MODE_OFF: Turn off VRR on the display */
VRR_DISPLAY_MODE_OFF,
+ /** @VRR_DISPLAY_MODE_MEDIA: Protocol-agnostic fixed rate media mode */
+ VRR_DISPLAY_MODE_MEDIA,
/** @VRR_DISPLAY_MODE_COUNT: Sentinel, not a valid mode, keep last */
VRR_DISPLAY_MODE_COUNT
};
@@ -1241,6 +1243,12 @@ struct drm_connector_hdmi_state {
* deliberately not requested by the helper.
*/
bool scrambler_needed;
+
+ /**
+ * @qms_tfr_byte: Target frame rate byte for HDMI QMS, or 0 if QMS is
+ * not in use.
+ */
+ u8 qms_tfr_byte;
};
/**
@@ -2270,6 +2278,12 @@ struct drm_connector_hdmi {
*/
bool scrambler_enabled;
+ /**
+ * @qms_supported: Whether the connector supports HDMI Quick Media
+ * Switching (QMS) on the source side, irrespective of display support.
+ */
+ bool qms_supported;
+
/**
* @scdc_work: Work item currently used to monitor sink-side scrambling
* status and retry setup if the sink resets it.
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 18/27] video/hdmi: Add support for QMS in VTEM EMP packing
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the necessary struct members and packing code to allow the
hdmi_emp_infoframe_vtem_pack function to write VTEM packets with HDMI
Quick Media Switching (QMS) data.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/video/hdmi.c | 6 ++++--
include/linux/hdmi.h | 2 ++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/drivers/video/hdmi.c b/drivers/video/hdmi.c
index ed9abc7ad228..c164f69a646a 100644
--- a/drivers/video/hdmi.c
+++ b/drivers/video/hdmi.c
@@ -1994,7 +1994,7 @@ ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
ptr[1] = BIT(7) | BIT(6);
ptr[2] = 0;
- ptr[3] = BIT(7) | BIT(2);
+ ptr[3] = BIT(7) | BIT(2) | (vtem->qms_en ? BIT(1) : 0);
ptr[4] = 0;
ptr[5] = 1;
@@ -2005,10 +2005,12 @@ ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
/* This is where the VTEM part starts */
ptr[10] = (vtem->fva_factor_m1 << 4) |
+ (vtem->qms_en ? BIT(2) : 0) |
(vtem->m_const ? BIT(1) : 0) |
(vtem->game_vrr_en ? BIT(0) : 0);
ptr[11] = vtem->base_vfront;
- ptr[12] = vtem->base_refresh_rate >> 8 & 0x3;
+ ptr[12] = (vtem->next_tfr << 3) |
+ (vtem->base_refresh_rate >> 8 & 0x3);
ptr[13] = vtem->base_refresh_rate & 0xFF;
return 14;
diff --git a/include/linux/hdmi.h b/include/linux/hdmi.h
index 71e2b36896af..8eaa127f0cb5 100644
--- a/include/linux/hdmi.h
+++ b/include/linux/hdmi.h
@@ -243,10 +243,12 @@ struct hdmi_emp_infoframe_vtem {
unsigned char length;
bool game_vrr_en;
+ bool qms_en;
bool m_const;
unsigned char fva_factor_m1;
unsigned char base_vfront;
u16 base_refresh_rate;
+ u8 next_tfr;
};
void hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame);
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 18/27] video/hdmi: Add support for QMS in VTEM EMP packing
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the necessary struct members and packing code to allow the
hdmi_emp_infoframe_vtem_pack function to write VTEM packets with HDMI
Quick Media Switching (QMS) data.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/video/hdmi.c | 6 ++++--
include/linux/hdmi.h | 2 ++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/drivers/video/hdmi.c b/drivers/video/hdmi.c
index ed9abc7ad228..c164f69a646a 100644
--- a/drivers/video/hdmi.c
+++ b/drivers/video/hdmi.c
@@ -1994,7 +1994,7 @@ ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
ptr[1] = BIT(7) | BIT(6);
ptr[2] = 0;
- ptr[3] = BIT(7) | BIT(2);
+ ptr[3] = BIT(7) | BIT(2) | (vtem->qms_en ? BIT(1) : 0);
ptr[4] = 0;
ptr[5] = 1;
@@ -2005,10 +2005,12 @@ ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
/* This is where the VTEM part starts */
ptr[10] = (vtem->fva_factor_m1 << 4) |
+ (vtem->qms_en ? BIT(2) : 0) |
(vtem->m_const ? BIT(1) : 0) |
(vtem->game_vrr_en ? BIT(0) : 0);
ptr[11] = vtem->base_vfront;
- ptr[12] = vtem->base_refresh_rate >> 8 & 0x3;
+ ptr[12] = (vtem->next_tfr << 3) |
+ (vtem->base_refresh_rate >> 8 & 0x3);
ptr[13] = vtem->base_refresh_rate & 0xFF;
return 14;
diff --git a/include/linux/hdmi.h b/include/linux/hdmi.h
index 71e2b36896af..8eaa127f0cb5 100644
--- a/include/linux/hdmi.h
+++ b/include/linux/hdmi.h
@@ -243,10 +243,12 @@ struct hdmi_emp_infoframe_vtem {
unsigned char length;
bool game_vrr_en;
+ bool qms_en;
bool m_const;
unsigned char fva_factor_m1;
unsigned char base_vfront;
u16 base_refresh_rate;
+ u8 next_tfr;
};
void hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame);
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 19/27] drm/connector: hdmi: Add QMS to VTEM EMP generation
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
If userspace indicates that VRR mode "Media" is desired and the
connector supports QMS, set the QMS related fields in the VTEM EMP
struct based on the connector state.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index 846f7a72fddc..407ad28c649f 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -890,11 +890,18 @@ static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connecto
break;
case VRR_DISPLAY_MODE_OFF:
break;
+ case VRR_DISPLAY_MODE_MEDIA:
+ if (!connector->hdmi.qms_supported)
+ return -EOPNOTSUPP;
+ vtem->qms_en = true;
+ vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
+ vtem->m_const = !vrr->dynamic;
+ break;
default:
return -EINVAL;
}
- if (vtem->game_vrr_en && !avi_vic) {
+ if ((vtem->game_vrr_en || vtem->qms_en) && !avi_vic) {
vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
vfront = crtc_state->adjusted_mode.crtc_vsync_start -
crtc_state->adjusted_mode.crtc_vdisplay;
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 19/27] drm/connector: hdmi: Add QMS to VTEM EMP generation
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
If userspace indicates that VRR mode "Media" is desired and the
connector supports QMS, set the QMS related fields in the VTEM EMP
struct based on the connector state.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index 846f7a72fddc..407ad28c649f 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -890,11 +890,18 @@ static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connecto
break;
case VRR_DISPLAY_MODE_OFF:
break;
+ case VRR_DISPLAY_MODE_MEDIA:
+ if (!connector->hdmi.qms_supported)
+ return -EOPNOTSUPP;
+ vtem->qms_en = true;
+ vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
+ vtem->m_const = !vrr->dynamic;
+ break;
default:
return -EINVAL;
}
- if (vtem->game_vrr_en && !avi_vic) {
+ if ((vtem->game_vrr_en || vtem->qms_en) && !avi_vic) {
vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
vfront = crtc_state->adjusted_mode.crtc_vsync_start -
crtc_state->adjusted_mode.crtc_vdisplay;
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 20/27] drm/connector: hdmi: Add QMS state validation and computation
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the necessary code to the HDMI state helpers' hdmi_validate_vrr
function in order to check and derive the state necessary for HDMI Quick
Media Switching (QMS) support.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 112 +++++++++++++++++++++++-
1 file changed, 109 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index 407ad28c649f..ba290eea7390 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -2,6 +2,7 @@
#include <linux/debugfs.h>
#include <linux/export.h>
+#include <linux/gcd.h>
#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
@@ -954,6 +955,26 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
return 0;
}
+static const struct {
+ u8 tfr_byte;
+ u32 rate_n;
+ u32 rate_d;
+} hdmi_qms_tfrs[] = {
+ { 1, 24000, 1001 },
+ { 2, 24, 1 },
+ { 3, 25, 1 },
+ { 4, 30000, 1001 },
+ { 5, 30, 1 },
+ { 6, 48000, 1001 },
+ { 7, 48, 1 },
+ { 8, 50, 1 },
+ { 9, 60000, 1001 },
+ { 10, 60, 1 },
+ { 11, 100, 1 },
+ { 12, 120000, 1001 },
+ { 13, 120, 1 },
+};
+
/**
* cmp_fraction - compares two fractional numbers
* @a: numerator of the first fraction
@@ -992,6 +1013,66 @@ static int cmp_fraction(u32 a, u32 b, u32 c, u32 d)
return 0;
}
+/**
+ * hdmi_validate_qms - Validate HDMI QMS state
+ * @crtc_state: pointer to a &struct drm_crtc_state to run validation on
+ * @info: pointer to a &struct drm_display_info of the accompanying connector
+ *
+ * Validate that the drm_crtc_vrr_state.vrr_min_numerator and
+ * drm_crtc_vrr_state.vrr_min_denominator are acceptable for HDMI QMS, and check
+ * that the display is HDMI QMS capable.
+ *
+ * Returns:
+ * - Negative errno on failure
+ * - the corresponding QMS signaling byte for the target frame rate otherwise
+ */
+static int hdmi_validate_qms(struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state,
+ const struct drm_display_info *info)
+{
+ u32 rate_n = crtc_state->vrr_state.vrr_min_n;
+ u32 rate_d = crtc_state->vrr_state.vrr_min_d;
+ u32 min_rate_n, max_rate;
+ u32 g, i;
+
+ if (conn_state->vrr_display_mode != VRR_DISPLAY_MODE_MEDIA)
+ return 0;
+
+ if (!drm_crtc_helper_vrr_is_fixed_rate(&crtc_state->vrr_state))
+ return -EINVAL;
+
+ if (info->hdmi.vrr_cap.qms_tfr_min)
+ min_rate_n = 24000;
+ else
+ min_rate_n = info->hdmi.vrr_cap.vrr_min * 1000;
+
+ if (info->hdmi.vrr_cap.qms_tfr_max)
+ max_rate = info->hdmi.vrr_cap.vrr_max;
+ else
+ max_rate = 60;
+
+ g = gcd(rate_n, rate_d);
+ if (g != 1) {
+ rate_n /= g;
+ rate_d /= g;
+ }
+
+ if (!rate_d)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(hdmi_qms_tfrs); i++) {
+ if (cmp_fraction(min_rate_n, 1001, hdmi_qms_tfrs[i].rate_n,
+ hdmi_qms_tfrs[i].rate_d) > 0)
+ continue;
+ if (rate_n == hdmi_qms_tfrs[i].rate_n &&
+ rate_d == hdmi_qms_tfrs[i].rate_d &&
+ cmp_fraction(rate_n, rate_d, max_rate, 1) <= 0)
+ return hdmi_qms_tfrs[i].tfr_byte;
+ }
+
+ return -EINVAL;
+}
+
static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
u32 rate_n, u32 rate_d, u16 *out_vtotal,
u32 *out_denom, u32 *out_frame_err)
@@ -1129,8 +1210,17 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
if (old_conn_state)
old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
- if (!info->hdmi.vrr_cap.supported)
- return -EOPNOTSUPP;
+ switch (new_conn_state->vrr_display_mode) {
+ case VRR_DISPLAY_MODE_GAME:
+ if (!info->hdmi.vrr_cap.supported)
+ return -EOPNOTSUPP;
+ break;
+ case VRR_DISPLAY_MODE_MEDIA:
+ if (!info->hdmi.vrr_cap.qms_capable ||
+ !connector->hdmi.qms_supported)
+ return -EOPNOTSUPP;
+ break;
+ }
mode_refresh = drm_mode_vrefresh(mode);
@@ -1145,7 +1235,10 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
max_vfreq_n = mode_refresh;
is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
- if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
+ if (is_fixed && ((info->hdmi.vrr_cap.qms_tfr_min &&
+ new_conn_state->vrr_display_mode == VRR_DISPLAY_MODE_MEDIA &&
+ connector->hdmi.qms_supported && info->hdmi.vrr_cap.qms_capable) ||
+ info->hdmi.vrr_cap.cinema_vrr))
min_vfreq_n = 24;
/* If the VRR target changed, make sure to reset the error accumulator */
@@ -1176,6 +1269,19 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
return ret;
}
+ ret = hdmi_validate_qms(new_crtc_state, new_conn_state, info);
+ if (ret < 0) {
+ drm_dbg_kms(dev, "Invalid QMS rate: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ new_conn_state->hdmi.qms_tfr_byte = ret;
+ if (!old_conn_state ||
+ new_conn_state->hdmi.qms_tfr_byte != old_conn_state->hdmi.qms_tfr_byte)
+ vrr_state->dynamic = true;
+ else
+ vrr_state->dynamic = false;
+
if (!is_fixed) {
vrr_state->dynamic = true;
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 20/27] drm/connector: hdmi: Add QMS state validation and computation
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the necessary code to the HDMI state helpers' hdmi_validate_vrr
function in order to check and derive the state necessary for HDMI Quick
Media Switching (QMS) support.
Co-developed-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Derek Foreman <derek.foreman@collabora.com>
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/display/drm_hdmi_state_helper.c | 112 +++++++++++++++++++++++-
1 file changed, 109 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
index 407ad28c649f..ba290eea7390 100644
--- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
+++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
@@ -2,6 +2,7 @@
#include <linux/debugfs.h>
#include <linux/export.h>
+#include <linux/gcd.h>
#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
@@ -954,6 +955,26 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
return 0;
}
+static const struct {
+ u8 tfr_byte;
+ u32 rate_n;
+ u32 rate_d;
+} hdmi_qms_tfrs[] = {
+ { 1, 24000, 1001 },
+ { 2, 24, 1 },
+ { 3, 25, 1 },
+ { 4, 30000, 1001 },
+ { 5, 30, 1 },
+ { 6, 48000, 1001 },
+ { 7, 48, 1 },
+ { 8, 50, 1 },
+ { 9, 60000, 1001 },
+ { 10, 60, 1 },
+ { 11, 100, 1 },
+ { 12, 120000, 1001 },
+ { 13, 120, 1 },
+};
+
/**
* cmp_fraction - compares two fractional numbers
* @a: numerator of the first fraction
@@ -992,6 +1013,66 @@ static int cmp_fraction(u32 a, u32 b, u32 c, u32 d)
return 0;
}
+/**
+ * hdmi_validate_qms - Validate HDMI QMS state
+ * @crtc_state: pointer to a &struct drm_crtc_state to run validation on
+ * @info: pointer to a &struct drm_display_info of the accompanying connector
+ *
+ * Validate that the drm_crtc_vrr_state.vrr_min_numerator and
+ * drm_crtc_vrr_state.vrr_min_denominator are acceptable for HDMI QMS, and check
+ * that the display is HDMI QMS capable.
+ *
+ * Returns:
+ * - Negative errno on failure
+ * - the corresponding QMS signaling byte for the target frame rate otherwise
+ */
+static int hdmi_validate_qms(struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state,
+ const struct drm_display_info *info)
+{
+ u32 rate_n = crtc_state->vrr_state.vrr_min_n;
+ u32 rate_d = crtc_state->vrr_state.vrr_min_d;
+ u32 min_rate_n, max_rate;
+ u32 g, i;
+
+ if (conn_state->vrr_display_mode != VRR_DISPLAY_MODE_MEDIA)
+ return 0;
+
+ if (!drm_crtc_helper_vrr_is_fixed_rate(&crtc_state->vrr_state))
+ return -EINVAL;
+
+ if (info->hdmi.vrr_cap.qms_tfr_min)
+ min_rate_n = 24000;
+ else
+ min_rate_n = info->hdmi.vrr_cap.vrr_min * 1000;
+
+ if (info->hdmi.vrr_cap.qms_tfr_max)
+ max_rate = info->hdmi.vrr_cap.vrr_max;
+ else
+ max_rate = 60;
+
+ g = gcd(rate_n, rate_d);
+ if (g != 1) {
+ rate_n /= g;
+ rate_d /= g;
+ }
+
+ if (!rate_d)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(hdmi_qms_tfrs); i++) {
+ if (cmp_fraction(min_rate_n, 1001, hdmi_qms_tfrs[i].rate_n,
+ hdmi_qms_tfrs[i].rate_d) > 0)
+ continue;
+ if (rate_n == hdmi_qms_tfrs[i].rate_n &&
+ rate_d == hdmi_qms_tfrs[i].rate_d &&
+ cmp_fraction(rate_n, rate_d, max_rate, 1) <= 0)
+ return hdmi_qms_tfrs[i].tfr_byte;
+ }
+
+ return -EINVAL;
+}
+
static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
u32 rate_n, u32 rate_d, u16 *out_vtotal,
u32 *out_denom, u32 *out_frame_err)
@@ -1129,8 +1210,17 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
if (old_conn_state)
old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
- if (!info->hdmi.vrr_cap.supported)
- return -EOPNOTSUPP;
+ switch (new_conn_state->vrr_display_mode) {
+ case VRR_DISPLAY_MODE_GAME:
+ if (!info->hdmi.vrr_cap.supported)
+ return -EOPNOTSUPP;
+ break;
+ case VRR_DISPLAY_MODE_MEDIA:
+ if (!info->hdmi.vrr_cap.qms_capable ||
+ !connector->hdmi.qms_supported)
+ return -EOPNOTSUPP;
+ break;
+ }
mode_refresh = drm_mode_vrefresh(mode);
@@ -1145,7 +1235,10 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
max_vfreq_n = mode_refresh;
is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
- if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
+ if (is_fixed && ((info->hdmi.vrr_cap.qms_tfr_min &&
+ new_conn_state->vrr_display_mode == VRR_DISPLAY_MODE_MEDIA &&
+ connector->hdmi.qms_supported && info->hdmi.vrr_cap.qms_capable) ||
+ info->hdmi.vrr_cap.cinema_vrr))
min_vfreq_n = 24;
/* If the VRR target changed, make sure to reset the error accumulator */
@@ -1176,6 +1269,19 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
return ret;
}
+ ret = hdmi_validate_qms(new_crtc_state, new_conn_state, info);
+ if (ret < 0) {
+ drm_dbg_kms(dev, "Invalid QMS rate: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ new_conn_state->hdmi.qms_tfr_byte = ret;
+ if (!old_conn_state ||
+ new_conn_state->hdmi.qms_tfr_byte != old_conn_state->hdmi.qms_tfr_byte)
+ vrr_state->dynamic = true;
+ else
+ vrr_state->dynamic = false;
+
if (!is_fixed) {
vrr_state->dynamic = true;
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 21/27] drm/rockchip: dw_hdmi_qp: Add QMS support
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the "Media" VRR display mode to the set of VRR display modes being
registered, and set the qms_supported flag in the connector's HDMI
sub-struct. The actual QMS functionality itself is implemented in the
common HDMI state helpers.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
index dcde50bca36b..215cf746dc41 100644
--- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
+++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
@@ -90,7 +90,8 @@
#define RK_COLOR_FMT_YUV420 0x3
#define RK_VRR_DISPLAY_MODES (BIT(VRR_DISPLAY_MODE_GAME) | \
- BIT(VRR_DISPLAY_MODE_OFF))
+ BIT(VRR_DISPLAY_MODE_OFF) | \
+ BIT(VRR_DISPLAY_MODE_MEDIA))
struct rockchip_hdmi_qp {
struct device *dev;
@@ -753,6 +754,8 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master,
return dev_err_probe(hdmi->dev, ret,
"Failed to attach 'vrr_display_mode' property\n");
+ connector->hdmi.qms_supported = true;
+
ret = drm_connector_attach_vrr_display_limits_properties(connector);
if (ret)
return dev_err_probe(hdmi->dev, ret,
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 21/27] drm/rockchip: dw_hdmi_qp: Add QMS support
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add the "Media" VRR display mode to the set of VRR display modes being
registered, and set the qms_supported flag in the connector's HDMI
sub-struct. The actual QMS functionality itself is implemented in the
common HDMI state helpers.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
index dcde50bca36b..215cf746dc41 100644
--- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
+++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
@@ -90,7 +90,8 @@
#define RK_COLOR_FMT_YUV420 0x3
#define RK_VRR_DISPLAY_MODES (BIT(VRR_DISPLAY_MODE_GAME) | \
- BIT(VRR_DISPLAY_MODE_OFF))
+ BIT(VRR_DISPLAY_MODE_OFF) | \
+ BIT(VRR_DISPLAY_MODE_MEDIA))
struct rockchip_hdmi_qp {
struct device *dev;
@@ -753,6 +754,8 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master,
return dev_err_probe(hdmi->dev, ret,
"Failed to attach 'vrr_display_mode' property\n");
+ connector->hdmi.qms_supported = true;
+
ret = drm_connector_attach_vrr_display_limits_properties(connector);
if (ret)
return dev_err_probe(hdmi->dev, ret,
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 22/27] drm/tests: hdmi: Add "Game Mode" VRR tests
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add a few tests to verify "Game Mode" VRR functionality.
One test checks that hdmi_validate_vrr succeeds if the sink is VRR
capable and VRR is enabled.
Another test checks that hdmi_validate_vrr fails if the sink isn't VRR
capable, but VRR is nevertheless enabled.
Yet another test checks that flush/vsync have cur_vtotal bounce between
base_vtotal and max_vtotal.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 263 +++++++++++++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.c | 140 +++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 1 +
3 files changed, 404 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 9953f5e661ce..def414085069 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -4,9 +4,12 @@
* Kunit test for drm_hdmi_state_helper functions
*/
+#include <linux/cleanup.h>
+
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_atomic_uapi.h>
+#include <drm/drm_crtc_helper.h>
#include <drm/drm_drv.h>
#include <drm/drm_edid.h>
#include <drm/drm_connector.h>
@@ -26,6 +29,15 @@
#include "drm_kunit_edid.h"
+static void drop_fini_modeset(struct drm_modeset_acquire_ctx *ctx)
+{
+ drm_modeset_drop_locks(ctx);
+ drm_modeset_acquire_fini(ctx);
+}
+
+DEFINE_GUARD(modeset_lock, struct drm_modeset_acquire_ctx *,
+ drm_modeset_acquire_init(_T, 0), drop_fini_modeset(_T));
+
struct drm_atomic_helper_connector_hdmi_priv {
struct drm_device drm;
struct drm_plane *plane;
@@ -289,6 +301,11 @@ __connector_hdmi_init(struct kunit *test,
drm_connector_helper_add(conn, &dummy_connector_helper_funcs);
drm_connector_attach_encoder(conn, enc);
+ ret = drm_connector_attach_vrr_display_mode_property(conn,
+ BIT(VRR_DISPLAY_MODE_GAME) |
+ BIT(VRR_DISPLAY_MODE_OFF));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
drm_mode_config_reset(drm);
if (edid_data && edid_len) {
@@ -3424,11 +3441,257 @@ static struct kunit_suite drm_atomic_helper_connector_hdmi_infoframes_test_suite
.test_cases = drm_atomic_helper_connector_hdmi_infoframes_tests,
};
+static void allocate_state_for_vrr_test(struct kunit *test,
+ struct drm_atomic_helper_connector_hdmi_priv *priv,
+ struct drm_modeset_acquire_ctx *ctx,
+ struct drm_display_mode *mode,
+ struct drm_atomic_commit **out_state)
+{
+ struct drm_atomic_commit *state;
+ int ret;
+
+retry:
+ ret = drm_kunit_helper_enable_crtc_connector(test, &priv->drm, priv->crtc,
+ &priv->connector, mode, ctx);
+ if (ret == -EDEADLK) {
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ state = drm_kunit_helper_atomic_state_alloc(test, &priv->drm, ctx);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, state);
+
+ *out_state = state;
+}
+
+
+static void prepare_state_for_vrr_test(struct kunit *test,
+ struct drm_atomic_helper_connector_hdmi_priv *priv,
+ struct drm_modeset_acquire_ctx *ctx,
+ struct drm_atomic_commit *state,
+ struct drm_crtc_state **out_crtc_state)
+{
+ struct drm_connector_state *conn_state;
+ struct drm_crtc_state *crtc_state;
+ int ret;
+
+retry_state:
+ conn_state = drm_atomic_get_connector_state(state, &priv->connector);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry_state;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ crtc_state = drm_atomic_get_crtc_state(state, priv->crtc);
+ if (PTR_ERR(crtc_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry_state;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, crtc_state);
+
+ *out_crtc_state = crtc_state;
+}
+
+/*
+ * Check that on a VRR-capable sink with VRR enabled, an atomic commit works.
+ */
+static void drm_test_check_hdmi_vrr(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ struct drm_crtc *crtc;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ crtc = priv->crtc;
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_GE(test, crtc_state->vrr_state.base_vtotal, preferred->crtc_vtotal);
+ KUNIT_EXPECT_GE(test, crtc_state->vrr_state.max_vtotal, preferred->crtc_vtotal);
+}
+
+/*
+ * Check that on a sink that's not VRR capable, enabling VRR and doing an atomic
+ * commit fails.
+ */
+static void drm_test_check_hdmi_vrr_sink_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+}
+
+/*
+ * Check that the drm_crtc_helper_vrr_flush() and drm_crtc_helper_vrr_vsync()
+ * helpers return the expected values on a VRR-capable sink with VRR enabled.
+ */
+static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal, preferred->crtc_vtotal);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * (drm_mode_vrefresh(preferred) / 30));
+
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+
+ /*
+ * Invoking drm_crtc_helper_vrr_flush() again should not change
+ * cur_vtotal, meaning it should return false.
+ */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.max_vtotal);
+
+ /* Same as above, double invocation should leave it unchanged. */
+
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.max_vtotal);
+}
+
+static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
+ KUNIT_CASE(drm_test_check_hdmi_vrr),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ { }
+};
+
+static struct kunit_suite drm_atomic_helper_connector_hdmi_vrr_test_suite = {
+ .name = "drm_atomic_helper_connector_hdmi_vrr",
+ .test_cases = drm_atomic_helper_connector_hdmi_vrr_tests,
+};
+
kunit_test_suites(
&drm_atomic_helper_connector_hdmi_check_test_suite,
&drm_atomic_helper_connector_hdmi_reset_test_suite,
&drm_atomic_helper_connector_hdmi_mode_valid_test_suite,
&drm_atomic_helper_connector_hdmi_infoframes_test_suite,
+ &drm_atomic_helper_connector_hdmi_vrr_test_suite,
);
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.c b/drivers/gpu/drm/tests/drm_kunit_edid.c
index c807ae0d9faa..8b78a8bf063d 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.c
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.c
@@ -990,6 +990,146 @@ const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[] = {
};
EXPORT_SYMBOL(test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: 30Hz to 120Hz
+ * CinemaVRR flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 43 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7e
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 10 1e 78
+ * 00 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 68
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 67
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7e
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports media rates below VRRmin (CinemaVRR, deprecated)
+ * VRRmin: 30 Hz
+ * VRRmax: 120 Hz
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0x68 Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * *** v- pretty sure this one's a bug in edid-decode ***
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8: CinemaVRR is deprecated and must be cleared.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_vrr[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x43, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7e, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x10, 0x1e, 0x78, 0x00, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x68
+};
+EXPORT_SYMBOL(test_edid_hdmi_vrr);
+
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
MODULE_DESCRIPTION("EDID Definitions for KUnit tests");
MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.h b/drivers/gpu/drm/tests/drm_kunit_edid.h
index 28b4df93a555..80dc15a8bcb6 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.h
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.h
@@ -12,5 +12,6 @@ extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_4k_yuv420_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[256];
+extern const unsigned char test_edid_hdmi_vrr[256];
#endif // DRM_KUNIT_EDID_H_
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 22/27] drm/tests: hdmi: Add "Game Mode" VRR tests
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add a few tests to verify "Game Mode" VRR functionality.
One test checks that hdmi_validate_vrr succeeds if the sink is VRR
capable and VRR is enabled.
Another test checks that hdmi_validate_vrr fails if the sink isn't VRR
capable, but VRR is nevertheless enabled.
Yet another test checks that flush/vsync have cur_vtotal bounce between
base_vtotal and max_vtotal.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 263 +++++++++++++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.c | 140 +++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 1 +
3 files changed, 404 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 9953f5e661ce..def414085069 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -4,9 +4,12 @@
* Kunit test for drm_hdmi_state_helper functions
*/
+#include <linux/cleanup.h>
+
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_atomic_uapi.h>
+#include <drm/drm_crtc_helper.h>
#include <drm/drm_drv.h>
#include <drm/drm_edid.h>
#include <drm/drm_connector.h>
@@ -26,6 +29,15 @@
#include "drm_kunit_edid.h"
+static void drop_fini_modeset(struct drm_modeset_acquire_ctx *ctx)
+{
+ drm_modeset_drop_locks(ctx);
+ drm_modeset_acquire_fini(ctx);
+}
+
+DEFINE_GUARD(modeset_lock, struct drm_modeset_acquire_ctx *,
+ drm_modeset_acquire_init(_T, 0), drop_fini_modeset(_T));
+
struct drm_atomic_helper_connector_hdmi_priv {
struct drm_device drm;
struct drm_plane *plane;
@@ -289,6 +301,11 @@ __connector_hdmi_init(struct kunit *test,
drm_connector_helper_add(conn, &dummy_connector_helper_funcs);
drm_connector_attach_encoder(conn, enc);
+ ret = drm_connector_attach_vrr_display_mode_property(conn,
+ BIT(VRR_DISPLAY_MODE_GAME) |
+ BIT(VRR_DISPLAY_MODE_OFF));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
drm_mode_config_reset(drm);
if (edid_data && edid_len) {
@@ -3424,11 +3441,257 @@ static struct kunit_suite drm_atomic_helper_connector_hdmi_infoframes_test_suite
.test_cases = drm_atomic_helper_connector_hdmi_infoframes_tests,
};
+static void allocate_state_for_vrr_test(struct kunit *test,
+ struct drm_atomic_helper_connector_hdmi_priv *priv,
+ struct drm_modeset_acquire_ctx *ctx,
+ struct drm_display_mode *mode,
+ struct drm_atomic_commit **out_state)
+{
+ struct drm_atomic_commit *state;
+ int ret;
+
+retry:
+ ret = drm_kunit_helper_enable_crtc_connector(test, &priv->drm, priv->crtc,
+ &priv->connector, mode, ctx);
+ if (ret == -EDEADLK) {
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ state = drm_kunit_helper_atomic_state_alloc(test, &priv->drm, ctx);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, state);
+
+ *out_state = state;
+}
+
+
+static void prepare_state_for_vrr_test(struct kunit *test,
+ struct drm_atomic_helper_connector_hdmi_priv *priv,
+ struct drm_modeset_acquire_ctx *ctx,
+ struct drm_atomic_commit *state,
+ struct drm_crtc_state **out_crtc_state)
+{
+ struct drm_connector_state *conn_state;
+ struct drm_crtc_state *crtc_state;
+ int ret;
+
+retry_state:
+ conn_state = drm_atomic_get_connector_state(state, &priv->connector);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry_state;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ crtc_state = drm_atomic_get_crtc_state(state, priv->crtc);
+ if (PTR_ERR(crtc_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(ctx);
+ if (!ret)
+ goto retry_state;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, crtc_state);
+
+ *out_crtc_state = crtc_state;
+}
+
+/*
+ * Check that on a VRR-capable sink with VRR enabled, an atomic commit works.
+ */
+static void drm_test_check_hdmi_vrr(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ struct drm_crtc *crtc;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ crtc = priv->crtc;
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_GE(test, crtc_state->vrr_state.base_vtotal, preferred->crtc_vtotal);
+ KUNIT_EXPECT_GE(test, crtc_state->vrr_state.max_vtotal, preferred->crtc_vtotal);
+}
+
+/*
+ * Check that on a sink that's not VRR capable, enabling VRR and doing an atomic
+ * commit fails.
+ */
+static void drm_test_check_hdmi_vrr_sink_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+}
+
+/*
+ * Check that the drm_crtc_helper_vrr_flush() and drm_crtc_helper_vrr_vsync()
+ * helpers return the expected values on a VRR-capable sink with VRR enabled.
+ */
+static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal, preferred->crtc_vtotal);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * (drm_mode_vrefresh(preferred) / 30));
+
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+
+ /*
+ * Invoking drm_crtc_helper_vrr_flush() again should not change
+ * cur_vtotal, meaning it should return false.
+ */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.max_vtotal);
+
+ /* Same as above, double invocation should leave it unchanged. */
+
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.max_vtotal);
+}
+
+static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
+ KUNIT_CASE(drm_test_check_hdmi_vrr),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ { }
+};
+
+static struct kunit_suite drm_atomic_helper_connector_hdmi_vrr_test_suite = {
+ .name = "drm_atomic_helper_connector_hdmi_vrr",
+ .test_cases = drm_atomic_helper_connector_hdmi_vrr_tests,
+};
+
kunit_test_suites(
&drm_atomic_helper_connector_hdmi_check_test_suite,
&drm_atomic_helper_connector_hdmi_reset_test_suite,
&drm_atomic_helper_connector_hdmi_mode_valid_test_suite,
&drm_atomic_helper_connector_hdmi_infoframes_test_suite,
+ &drm_atomic_helper_connector_hdmi_vrr_test_suite,
);
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.c b/drivers/gpu/drm/tests/drm_kunit_edid.c
index c807ae0d9faa..8b78a8bf063d 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.c
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.c
@@ -990,6 +990,146 @@ const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[] = {
};
EXPORT_SYMBOL(test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: 30Hz to 120Hz
+ * CinemaVRR flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 43 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7e
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 10 1e 78
+ * 00 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 68
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 67
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7e
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports media rates below VRRmin (CinemaVRR, deprecated)
+ * VRRmin: 30 Hz
+ * VRRmax: 120 Hz
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0x68 Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * *** v- pretty sure this one's a bug in edid-decode ***
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8: CinemaVRR is deprecated and must be cleared.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_vrr[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x43, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7e, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x10, 0x1e, 0x78, 0x00, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x68
+};
+EXPORT_SYMBOL(test_edid_hdmi_vrr);
+
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
MODULE_DESCRIPTION("EDID Definitions for KUnit tests");
MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.h b/drivers/gpu/drm/tests/drm_kunit_edid.h
index 28b4df93a555..80dc15a8bcb6 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.h
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.h
@@ -12,5 +12,6 @@ extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_4k_yuv420_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[256];
+extern const unsigned char test_edid_hdmi_vrr[256];
#endif // DRM_KUNIT_EDID_H_
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 23/27] drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add tests to verify the functionality of the
VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR} properties and the HDMI state
helpers that make use of them.
This includes tests for fixed rate scenarios, as well as a test for a
constrained game mode scenario. A parametric test case checks that
various erroneous target rates are rejected.
Proper handling of CinemaVRR is tested as well.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 386 +++++++++++++++++++++
1 file changed, 386 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index def414085069..4d9bcab57778 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -3674,10 +3674,396 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
crtc_state->vrr_state.max_vtotal);
}
+/*
+ * Check that on a VRR-capable sink with VRR enabled and a fixed target rate
+ * set, an atomic commit works, and the derived vtotals are as expected.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.cinema_vrr);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ /* Equivalent fraction, expressed differently to pose a hurdle */
+ crtc_state->vrr_state.vrr_max_n = 40000;
+ crtc_state->vrr_state.vrr_max_d = 1000;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 40);
+
+ /* Minimum rate is 24Hz because of the CinemaVRR flag */
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 24);
+}
+
+/*
+ * Check that the drm_hdmi_helper_vrr_flush() and drm_hdmi_helper_vrr_vsync()
+ * helpers return the expected values on a VRR-capable sink with VRR enabled
+ * and a fixed target set.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate_flush_vsync(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ scoped_guard(modeset_lock, &ctx) {
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, expressed differently to pose a hurdle */
+ crtc_state->vrr_state.vrr_max_n = 80000;
+ crtc_state->vrr_state.vrr_max_d = 2002;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ mult_frac(preferred->crtc_vtotal, mode_refresh * 1000, 39960));
+
+ /* Fixed rate case: flush should never trigger a vtotal update. */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal, 0);
+
+ /*
+ * Invoking drm_hdmi_helper_vrr_flush() again should not change
+ * cur_vtotal, meaning it should return false.
+ */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal, 0);
+
+ /* First vsync will set cur_vtotal. */
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ /* First vsync with fixed rate will still be dynamic. */
+ KUNIT_EXPECT_TRUE(test, crtc_state->vrr_state.dynamic);
+ }
+
+ /* Try a second commit to evaluate the dynamic flag */
+ scoped_guard(modeset_lock, &ctx) {
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry_2:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry_2;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ KUNIT_EXPECT_FALSE(test, crtc_state->vrr_state.dynamic);
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ /* Subsequent vsyncs with fixed rate won't be dynamic. */
+ KUNIT_EXPECT_FALSE(test, crtc_state->vrr_state.dynamic);
+ }
+}
+
+struct vrr_target_error_params {
+ unsigned int vrr_min_n;
+ unsigned int vrr_min_d;
+ unsigned int vrr_max_n;
+ unsigned int vrr_max_d;
+ bool cinema_vrr_override;
+ const char *desc;
+};
+
+static struct vrr_target_error_params vrr_target_errors[] = {
+ { 26, 1, 26 * 2000, 2000, false, "VRR target <= min rate should fail" },
+ { 200, 1, 200, 1, true, "VRR target <= max rate should fail" },
+ { 60, 1, 40, 1, true, "VRR max target <= VRR min target should fail" },
+ { 40, 1, 60, 0, true, "VRR max target denominator of 0 should fail" },
+ { 40, 0, 60, 1, true, "VRR min target denominator of 0 should fail" },
+ { 25000, 1001, 30, 1, true, "CinemaVRR with non-fixed rate should fail" },
+ { 0, 1000, 60, 1, true, "Minimum-Incomplete range should fail" },
+ { 40, 1, 0, 0, false, "Maximum-Incomplete range should fail" },
+};
+
+KUNIT_ARRAY_PARAM_DESC(check_vrr_target_errors, vrr_target_errors, desc);
+
+/*
+ * Check that on a VRR-capable sink with VRR enabled and various erroneous
+ * combinations of target rates set, an atomic commit fails.
+ */
+static void drm_test_check_hdmi_vrr_target_rate_fail(struct kunit *test)
+{
+ const struct vrr_target_error_params *params = test->param_value;
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ /*
+ * Allow individual test parameters to play with the CinemaVRR flag
+ * without needing to bring their own modified EDID.
+ */
+ conn->display_info.hdmi.vrr_cap.cinema_vrr = params->cinema_vrr_override;
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = params->vrr_min_n;
+ crtc_state->vrr_state.vrr_min_d = params->vrr_min_d;
+ crtc_state->vrr_state.vrr_max_n = params->vrr_max_n;
+ crtc_state->vrr_state.vrr_max_d = params->vrr_max_d;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+
+ drm_modeset_drop_locks(&ctx);
+ drm_modeset_acquire_fini(&ctx);
+}
+
+/*
+ * Check that on a VRR-capable sink that has the CinemaVRR flag set, with VRR
+ * enabled and a fixed target rate set, an atomic commit succeeds at a rate
+ * only possible if CinemaVRR is supported and a fixed target is used.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.cinema_vrr);
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ /* 24/1.001 Hz should work */
+ crtc_state->vrr_state.vrr_min_n = 24000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ crtc_state->vrr_state.vrr_max_n = 24000;
+ crtc_state->vrr_state.vrr_max_d = 1001;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ mult_frac(preferred->crtc_vtotal, mode_refresh * 1000, 23976));
+}
+
+/*
+ * Check that on a VRR-capable sink, an atomic commit with a VRR minimum and
+ * maximum target rate that's more narrow than the monitor's target rate
+ * succeeds and produces the expected derived values.
+ */
+static void drm_test_check_hdmi_vrr_constrained_rate(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 80;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 80);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 40);
+}
+
static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_flush_vsync),
+ KUNIT_CASE_PARAM(drm_test_check_hdmi_vrr_target_rate_fail,
+ check_vrr_target_errors_gen_params),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_constrained_rate),
{ }
};
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 23/27] drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add tests to verify the functionality of the
VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR} properties and the HDMI state
helpers that make use of them.
This includes tests for fixed rate scenarios, as well as a test for a
constrained game mode scenario. A parametric test case checks that
various erroneous target rates are rejected.
Proper handling of CinemaVRR is tested as well.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 386 +++++++++++++++++++++
1 file changed, 386 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index def414085069..4d9bcab57778 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -3674,10 +3674,396 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
crtc_state->vrr_state.max_vtotal);
}
+/*
+ * Check that on a VRR-capable sink with VRR enabled and a fixed target rate
+ * set, an atomic commit works, and the derived vtotals are as expected.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.cinema_vrr);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ /* Equivalent fraction, expressed differently to pose a hurdle */
+ crtc_state->vrr_state.vrr_max_n = 40000;
+ crtc_state->vrr_state.vrr_max_d = 1000;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 40);
+
+ /* Minimum rate is 24Hz because of the CinemaVRR flag */
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 24);
+}
+
+/*
+ * Check that the drm_hdmi_helper_vrr_flush() and drm_hdmi_helper_vrr_vsync()
+ * helpers return the expected values on a VRR-capable sink with VRR enabled
+ * and a fixed target set.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate_flush_vsync(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, priv->connector.display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(&priv->connector);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ scoped_guard(modeset_lock, &ctx) {
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, expressed differently to pose a hurdle */
+ crtc_state->vrr_state.vrr_max_n = 80000;
+ crtc_state->vrr_state.vrr_max_d = 2002;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ mult_frac(preferred->crtc_vtotal, mode_refresh * 1000, 39960));
+
+ /* Fixed rate case: flush should never trigger a vtotal update. */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal, 0);
+
+ /*
+ * Invoking drm_hdmi_helper_vrr_flush() again should not change
+ * cur_vtotal, meaning it should return false.
+ */
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal, 0);
+
+ /* First vsync will set cur_vtotal. */
+ KUNIT_EXPECT_TRUE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ /* First vsync with fixed rate will still be dynamic. */
+ KUNIT_EXPECT_TRUE(test, crtc_state->vrr_state.dynamic);
+ }
+
+ /* Try a second commit to evaluate the dynamic flag */
+ scoped_guard(modeset_lock, &ctx) {
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry_2:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry_2;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_flush(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ KUNIT_EXPECT_FALSE(test, crtc_state->vrr_state.dynamic);
+ KUNIT_EXPECT_FALSE(test, drm_crtc_helper_vrr_vsync(crtc_state));
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.cur_vtotal,
+ crtc_state->vrr_state.base_vtotal);
+ /* Subsequent vsyncs with fixed rate won't be dynamic. */
+ KUNIT_EXPECT_FALSE(test, crtc_state->vrr_state.dynamic);
+ }
+}
+
+struct vrr_target_error_params {
+ unsigned int vrr_min_n;
+ unsigned int vrr_min_d;
+ unsigned int vrr_max_n;
+ unsigned int vrr_max_d;
+ bool cinema_vrr_override;
+ const char *desc;
+};
+
+static struct vrr_target_error_params vrr_target_errors[] = {
+ { 26, 1, 26 * 2000, 2000, false, "VRR target <= min rate should fail" },
+ { 200, 1, 200, 1, true, "VRR target <= max rate should fail" },
+ { 60, 1, 40, 1, true, "VRR max target <= VRR min target should fail" },
+ { 40, 1, 60, 0, true, "VRR max target denominator of 0 should fail" },
+ { 40, 0, 60, 1, true, "VRR min target denominator of 0 should fail" },
+ { 25000, 1001, 30, 1, true, "CinemaVRR with non-fixed rate should fail" },
+ { 0, 1000, 60, 1, true, "Minimum-Incomplete range should fail" },
+ { 40, 1, 0, 0, false, "Maximum-Incomplete range should fail" },
+};
+
+KUNIT_ARRAY_PARAM_DESC(check_vrr_target_errors, vrr_target_errors, desc);
+
+/*
+ * Check that on a VRR-capable sink with VRR enabled and various erroneous
+ * combinations of target rates set, an atomic commit fails.
+ */
+static void drm_test_check_hdmi_vrr_target_rate_fail(struct kunit *test)
+{
+ const struct vrr_target_error_params *params = test->param_value;
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ /*
+ * Allow individual test parameters to play with the CinemaVRR flag
+ * without needing to bring their own modified EDID.
+ */
+ conn->display_info.hdmi.vrr_cap.cinema_vrr = params->cinema_vrr_override;
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = params->vrr_min_n;
+ crtc_state->vrr_state.vrr_min_d = params->vrr_min_d;
+ crtc_state->vrr_state.vrr_max_n = params->vrr_max_n;
+ crtc_state->vrr_state.vrr_max_d = params->vrr_max_d;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+
+ drm_modeset_drop_locks(&ctx);
+ drm_modeset_acquire_fini(&ctx);
+}
+
+/*
+ * Check that on a VRR-capable sink that has the CinemaVRR flag set, with VRR
+ * enabled and a fixed target rate set, an atomic commit succeeds at a rate
+ * only possible if CinemaVRR is supported and a fixed target is used.
+ */
+static void drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.cinema_vrr);
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+
+ /* 24/1.001 Hz should work */
+ crtc_state->vrr_state.vrr_min_n = 24000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ crtc_state->vrr_state.vrr_max_n = 24000;
+ crtc_state->vrr_state.vrr_max_d = 1001;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ mult_frac(preferred->crtc_vtotal, mode_refresh * 1000, 23976));
+}
+
+/*
+ * Check that on a VRR-capable sink, an atomic commit with a VRR minimum and
+ * maximum target rate that's more narrow than the monitor's target rate
+ * succeeds and produces the expected derived values.
+ */
+static void drm_test_check_hdmi_vrr_constrained_rate(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int mode_refresh;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ mode_refresh = drm_mode_vrefresh(preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 40;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 80;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, priv->crtc);
+ KUNIT_ASSERT_NOT_NULL(test, crtc_state);
+
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.base_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 80);
+ KUNIT_EXPECT_EQ(test, crtc_state->vrr_state.max_vtotal,
+ preferred->crtc_vtotal * mode_refresh / 40);
+}
+
static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_flush_vsync),
+ KUNIT_CASE_PARAM(drm_test_check_hdmi_vrr_target_rate_fail,
+ check_vrr_target_errors_gen_params),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_constrained_rate),
{ }
};
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 24/27] drm/tests: hdmi: Add Quick Media Switching tests
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add KUnit tests to validate the HDMI Quick Media Switching (QMS)
functionality in the HDMI state helpers.
This includes tests to validate that the QMS TFR min and QMS TFR max
flags are respected.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 574 ++++++++++++++++++++-
drivers/gpu/drm/tests/drm_kunit_edid.c | 280 ++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 2 +
3 files changed, 855 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 4d9bcab57778..4c7e563ca51d 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -303,8 +303,10 @@ __connector_hdmi_init(struct kunit *test,
ret = drm_connector_attach_vrr_display_mode_property(conn,
BIT(VRR_DISPLAY_MODE_GAME) |
- BIT(VRR_DISPLAY_MODE_OFF));
+ BIT(VRR_DISPLAY_MODE_OFF) |
+ BIT(VRR_DISPLAY_MODE_MEDIA));
KUNIT_ASSERT_EQ(test, ret, 0);
+ conn->hdmi.qms_supported = true;
drm_mode_config_reset(drm);
@@ -4054,6 +4056,568 @@ static void drm_test_check_hdmi_vrr_constrained_rate(struct kunit *test)
preferred->crtc_vtotal * mode_refresh / 40);
}
+/*
+ * Check that on a QMS-capable sink, an atomic commit with VRR display mode
+ * set to "Media" succeeds, and produces the right connector state values.
+ */
+static void drm_test_check_hdmi_vrr_qms(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, as an added hurdle */
+ crtc_state->vrr_state.vrr_max_n = 48000 * 2;
+ crtc_state->vrr_state.vrr_max_d = 1001 * 2;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 6);
+}
+
+/*
+ * Check that on a QMS-capable sink, an atomic commit with VRR display mode
+ * set to "Media" fails if the requested target rate is not supported by QMS.
+ */
+static void drm_test_check_hdmi_vrr_qms_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 67;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 67;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink, an atomic commit with the VRR display mode
+ * set to "Media" fails if the requested target rate is not a fixed rate.
+ */
+static void drm_test_check_hdmi_vrr_qms_nonfixed_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 30;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 60;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink with QMS TFR min set, an atomic commit with
+ * the VRR display mode set to "Media" succeeds even if the target rate is
+ * below the monitor's range, and that it produces the right connector state
+ * values.
+ */
+static void drm_test_check_hdmi_vrr_qms_tfr_min(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_tfr_min);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+ /*
+ * The CinemaVRR flag provides equivalent functionality to QMS Min TFR
+ * for non-QMS VRR, so it should be false to ensure that this unit test
+ * tests the right thing.
+ */
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.cinema_vrr);
+
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 24;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 24;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 2);
+}
+
+/*
+ * Check that on a QMS-capable sink without the QMS max TFR flag, an atomic
+ * commit with QMS enabled and a target rate above 60Hz fails.
+ */
+static void drm_test_check_hdmi_vrr_qms_no_tfr_max_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.qms_tfr_max);
+
+ KUNIT_ASSERT_LE(test, 100, conn->display_info.hdmi.vrr_cap.vrr_max);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 100;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 100;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink with the QMS max TFR flag, an atomic commit
+ * with QMS enabled and a target rate above 60Hz succeeds.
+ */
+static void drm_test_check_hdmi_vrr_qms_tfr_max(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ KUNIT_ASSERT_LE(test, 100, conn->display_info.hdmi.vrr_cap.vrr_max);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ /* Manual override to not have to define an additional EDID. */
+ conn->display_info.hdmi.vrr_cap.qms_tfr_max = true;
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 1000; /* Intentional tomfoolery. */
+ crtc_state->vrr_state.vrr_min_d = 10;
+ crtc_state->vrr_state.vrr_max_n = 100;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 11);
+}
+
+/*
+ * Check that on a QMS-capable but VRR-incapable sink, an atomic commit with VRR
+ * display mode set to "Media" succeeds, and produces the right connector state
+ * values.
+ */
+static void drm_test_check_hdmi_vrr_qms_only(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_qms_only);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, as an added hurdle */
+ crtc_state->vrr_state.vrr_max_n = 48000 * 2;
+ crtc_state->vrr_state.vrr_max_d = 1001 * 2;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 6);
+}
+
+/*
+ * Check that on a QMS-capable but VRR-incapable sink, an atomic commit with VRR
+ * display mode set to "Game" fails.
+ */
+static void drm_test_check_hdmi_vrr_qms_only_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_qms_only);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 48;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_GAME;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+}
+
static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
@@ -4064,6 +4628,14 @@ static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
check_vrr_target_errors_gen_params),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_constrained_rate),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_nonfixed_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_tfr_min),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_no_tfr_max_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_tfr_max),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_only),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_only_fail),
{ }
};
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.c b/drivers/gpu/drm/tests/drm_kunit_edid.c
index 8b78a8bf063d..03eb809909f0 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.c
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.c
@@ -1130,6 +1130,286 @@ const unsigned char test_edid_hdmi_vrr[] = {
};
EXPORT_SYMBOL(test_edid_hdmi_vrr);
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: 30Hz to 120Hz
+ * CinemaVRR flag is unset
+ * QMS flag is set
+ * QMS TFRmin flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 45 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7c
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 40 1e 78
+ * 10 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 28
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 69
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7c
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports QMS
+ * VRRmin: 30 Hz
+ * VRRmax: 120 Hz
+ * Supports QMS TFRmin
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0x28 Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_vrr_qms[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7c, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x40, 0x1e, 0x78, 0x10, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x28
+};
+EXPORT_SYMBOL(test_edid_hdmi_vrr_qms);
+
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: none
+ * CinemaVRR flag is unset
+ * QMS flag is set
+ * QMS TFRmin flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 45 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7c
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 40 00 00
+ * 10 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 be
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 69
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7c
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports QMS
+ * Supports QMS TFRmin
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0xbe Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_qms_only[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7c, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xbe
+};
+EXPORT_SYMBOL(test_edid_hdmi_qms_only);
+
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
MODULE_DESCRIPTION("EDID Definitions for KUnit tests");
MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.h b/drivers/gpu/drm/tests/drm_kunit_edid.h
index 80dc15a8bcb6..e993bbd5427b 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.h
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.h
@@ -13,5 +13,7 @@ extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_4k_yuv420_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_vrr[256];
+extern const unsigned char test_edid_hdmi_vrr_qms[256];
+extern const unsigned char test_edid_hdmi_qms_only[256];
#endif // DRM_KUNIT_EDID_H_
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 24/27] drm/tests: hdmi: Add Quick Media Switching tests
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Add KUnit tests to validate the HDMI Quick Media Switching (QMS)
functionality in the HDMI state helpers.
This includes tests to validate that the QMS TFR min and QMS TFR max
flags are respected.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 574 ++++++++++++++++++++-
drivers/gpu/drm/tests/drm_kunit_edid.c | 280 ++++++++++
drivers/gpu/drm/tests/drm_kunit_edid.h | 2 +
3 files changed, 855 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 4d9bcab57778..4c7e563ca51d 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -303,8 +303,10 @@ __connector_hdmi_init(struct kunit *test,
ret = drm_connector_attach_vrr_display_mode_property(conn,
BIT(VRR_DISPLAY_MODE_GAME) |
- BIT(VRR_DISPLAY_MODE_OFF));
+ BIT(VRR_DISPLAY_MODE_OFF) |
+ BIT(VRR_DISPLAY_MODE_MEDIA));
KUNIT_ASSERT_EQ(test, ret, 0);
+ conn->hdmi.qms_supported = true;
drm_mode_config_reset(drm);
@@ -4054,6 +4056,568 @@ static void drm_test_check_hdmi_vrr_constrained_rate(struct kunit *test)
preferred->crtc_vtotal * mode_refresh / 40);
}
+/*
+ * Check that on a QMS-capable sink, an atomic commit with VRR display mode
+ * set to "Media" succeeds, and produces the right connector state values.
+ */
+static void drm_test_check_hdmi_vrr_qms(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, as an added hurdle */
+ crtc_state->vrr_state.vrr_max_n = 48000 * 2;
+ crtc_state->vrr_state.vrr_max_d = 1001 * 2;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 6);
+}
+
+/*
+ * Check that on a QMS-capable sink, an atomic commit with VRR display mode
+ * set to "Media" fails if the requested target rate is not supported by QMS.
+ */
+static void drm_test_check_hdmi_vrr_qms_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 67;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 67;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink, an atomic commit with the VRR display mode
+ * set to "Media" fails if the requested target rate is not a fixed rate.
+ */
+static void drm_test_check_hdmi_vrr_qms_nonfixed_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 30;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 60;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink with QMS TFR min set, an atomic commit with
+ * the VRR display mode set to "Media" succeeds even if the target rate is
+ * below the monitor's range, and that it produces the right connector state
+ * values.
+ */
+static void drm_test_check_hdmi_vrr_qms_tfr_min(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_tfr_min);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+ /*
+ * The CinemaVRR flag provides equivalent functionality to QMS Min TFR
+ * for non-QMS VRR, so it should be false to ensure that this unit test
+ * tests the right thing.
+ */
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.cinema_vrr);
+
+ KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 24;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 24;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 2);
+}
+
+/*
+ * Check that on a QMS-capable sink without the QMS max TFR flag, an atomic
+ * commit with QMS enabled and a target rate above 60Hz fails.
+ */
+static void drm_test_check_hdmi_vrr_qms_no_tfr_max_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.qms_tfr_max);
+
+ KUNIT_ASSERT_LE(test, 100, conn->display_info.hdmi.vrr_cap.vrr_max);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 100;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 100;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_LT(test, ret, 0);
+}
+
+/*
+ * Check that on a QMS-capable sink with the QMS max TFR flag, an atomic commit
+ * with QMS enabled and a target rate above 60Hz succeeds.
+ */
+static void drm_test_check_hdmi_vrr_qms_tfr_max(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr_qms);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ KUNIT_ASSERT_LE(test, 100, conn->display_info.hdmi.vrr_cap.vrr_max);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ /* Manual override to not have to define an additional EDID. */
+ conn->display_info.hdmi.vrr_cap.qms_tfr_max = true;
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 1000; /* Intentional tomfoolery. */
+ crtc_state->vrr_state.vrr_min_d = 10;
+ crtc_state->vrr_state.vrr_max_n = 100;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 11);
+}
+
+/*
+ * Check that on a QMS-capable but VRR-incapable sink, an atomic commit with VRR
+ * display mode set to "Media" succeeds, and produces the right connector state
+ * values.
+ */
+static void drm_test_check_hdmi_vrr_qms_only(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_qms_only);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48000;
+ crtc_state->vrr_state.vrr_min_d = 1001;
+ /* Equivalent fraction, as an added hurdle */
+ crtc_state->vrr_state.vrr_max_n = 48000 * 2;
+ crtc_state->vrr_state.vrr_max_d = 1001 * 2;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_MEDIA;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ conn_state = drm_atomic_get_new_connector_state(state, conn);
+ KUNIT_ASSERT_NOT_NULL(test, conn_state);
+
+ KUNIT_EXPECT_EQ(test, conn_state->hdmi.qms_tfr_byte, 6);
+}
+
+/*
+ * Check that on a QMS-capable but VRR-incapable sink, an atomic commit with VRR
+ * display mode set to "Game" fails.
+ */
+static void drm_test_check_hdmi_vrr_qms_only_fail(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_connector_state *conn_state;
+ struct drm_modeset_acquire_ctx ctx;
+ struct drm_display_mode *preferred;
+ struct drm_crtc_state *crtc_state;
+ struct drm_atomic_commit *state;
+ struct drm_connector *conn;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_qms_only);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.qms_capable);
+ KUNIT_ASSERT_TRUE(test, conn->hdmi.qms_supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ guard(modeset_lock)(&ctx);
+
+ allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
+
+retry:
+ prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
+
+ crtc_state->vrr_enabled = true;
+ crtc_state->vrr_state.vrr_min_n = 48;
+ crtc_state->vrr_state.vrr_min_d = 1;
+ crtc_state->vrr_state.vrr_max_n = 48;
+ crtc_state->vrr_state.vrr_max_d = 1;
+
+ conn_state = drm_atomic_get_connector_state(state, conn);
+ if (PTR_ERR(conn_state) == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conn_state);
+
+ conn_state->vrr_display_mode = VRR_DISPLAY_MODE_GAME;
+
+ ret = drm_atomic_commit(state);
+ if (ret == -EDEADLK) {
+ drm_atomic_commit_clear(state);
+ ret = drm_modeset_backoff(&ctx);
+ if (!ret)
+ goto retry;
+ }
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+}
+
static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
@@ -4064,6 +4628,14 @@ static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
check_vrr_target_errors_gen_params),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_cinema_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_constrained_rate),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_nonfixed_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_tfr_min),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_no_tfr_max_fail),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_tfr_max),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_only),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_qms_only_fail),
{ }
};
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.c b/drivers/gpu/drm/tests/drm_kunit_edid.c
index 8b78a8bf063d..03eb809909f0 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.c
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.c
@@ -1130,6 +1130,286 @@ const unsigned char test_edid_hdmi_vrr[] = {
};
EXPORT_SYMBOL(test_edid_hdmi_vrr);
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: 30Hz to 120Hz
+ * CinemaVRR flag is unset
+ * QMS flag is set
+ * QMS TFRmin flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 45 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7c
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 40 1e 78
+ * 10 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 28
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 69
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7c
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports QMS
+ * VRRmin: 30 Hz
+ * VRRmax: 120 Hz
+ * Supports QMS TFRmin
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0x28 Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_vrr_qms[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7c, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x40, 0x1e, 0x78, 0x10, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x28
+};
+EXPORT_SYMBOL(test_edid_hdmi_vrr_qms);
+
+/*
+ * Max resolution: 3840x2160@60Hz with YUV420
+ * Max BPC: 16 for all modes
+ * Max TMDS clock: <340MHz, so set to 0
+ * VRR range: none
+ * CinemaVRR flag is unset
+ * QMS flag is set
+ * QMS TFRmin flag is set
+ *
+ * edid-decode (hex):
+ *
+ * 00 ff ff ff ff ff ff 00 31 d8 45 00 00 00 00 00
+ * 01 24 01 03 80 60 36 78 0f ee 91 a3 54 4c 99 26
+ * 0f 50 54 20 00 00 01 01 01 01 01 01 01 01 01 01
+ * 01 01 01 01 01 01 04 74 80 18 71 38 2d 40 58 2c
+ * 45 00 c0 1c 32 00 00 1e 04 74 00 30 f2 70 5a 80
+ * b0 58 8a 00 c0 1c 32 00 00 1e 00 00 00 fc 00 54
+ * 65 73 74 20 45 44 49 44 0a 20 20 20 00 00 00 fd
+ * 00 18 78 18 87 22 00 0a 20 20 20 20 20 20 01 7c
+ *
+ * 02 03 29 31 42 3f 5f 6d 03 0c 00 10 00 78 00 20
+ * 00 00 00 20 61 6d d8 5d c4 01 00 80 07 40 00 00
+ * 10 00 00 e2 0e 61 e2 00 ed 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 be
+ *
+ * ----------------
+ *
+ * Block 0, Base EDID:
+ * EDID Structure Version & Revision: 1.3
+ * Vendor & Product Identification:
+ * Manufacturer: LNX
+ * Model: 69
+ * Made in: week 1 of 2026
+ * Basic Display Parameters & Features:
+ * Digital display
+ * Maximum image size: 96 cm x 54 cm
+ * Gamma: 2.20
+ * RGB color display
+ * Default (sRGB) color space is primary color space
+ * First detailed timing is the preferred timing
+ * Supports GTF timings within operating range
+ * Color Characteristics:
+ * Red : 0.6396, 0.3300
+ * Green: 0.2998, 0.5996
+ * Blue : 0.1503, 0.0595
+ * White: 0.3125, 0.3291
+ * Established Timings I & II:
+ * DMT 0x04: 640x480 59.940476 Hz 4:3 31.469 kHz 25.175000 MHz
+ * Standard Timings: none
+ * Detailed Timing Descriptors:
+ * DTD 1: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 88 Hsync 44 Hback 148 Hpol P
+ * Vfront 4 Vsync 5 Vback 36 Vpol P
+ * DTD 2: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz (960 mm x 540 mm)
+ * Hfront 176 Hsync 88 Hback 296 Hpol P
+ * Vfront 8 Vsync 10 Vback 72 Vpol P
+ * Display Product Name: 'Test EDID'
+ * Display Range Limits:
+ * Monitor ranges (GTF): 24-120 Hz V, 24-135 kHz H, max dotclock 340 MHz
+ * Extension blocks: 1
+ * Checksum: 0x7c
+ *
+ * ----------------
+ *
+ * Block 1, CTA-861 Extension Block:
+ * Revision: 3
+ * Supports YCbCr 4:4:4
+ * Supports YCbCr 4:2:2
+ * Native detailed modes: 1
+ * Video Data Block:
+ * VIC 63: 1920x1080 120.000000 Hz 16:9 135.000 kHz 297.000000 MHz
+ * VIC 95: 3840x2160 30.000000 Hz 16:9 67.500 kHz 297.000000 MHz
+ * Vendor-Specific Data Block (HDMI), OUI 00-0C-03:
+ * Source physical address: 1.0.0.0
+ * DC_48bit
+ * DC_36bit
+ * DC_30bit
+ * DC_Y444
+ * Maximum TMDS clock: 0 MHz
+ * Extended HDMI video details:
+ * Vendor-Specific Data Block (HDMI Forum), OUI C4-5D-D8:
+ * Version: 1
+ * SCDC Present
+ * Supports 16-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 12-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports 10-bits/component Deep Color 4:2:0 Pixel Encoding
+ * Supports QMS
+ * Supports QMS TFRmin
+ * YCbCr 4:2:0 Video Data Block:
+ * VIC 97: 3840x2160 60.000000 Hz 16:9 135.000 kHz 594.000000 MHz
+ * Video Capability Data Block:
+ * YCbCr quantization: Selectable (via AVI YQ)
+ * RGB quantization: Selectable (via AVI Q)
+ * PT scan behavior: Always Underscanned
+ * IT scan behavior: Supports both over- and underscan
+ * CE scan behavior: Always Overscanned
+ * Checksum: 0xbe Unused space in Extension Block: 86 bytes
+ *
+ * ----------------
+ *
+ * edid-decode 1.32.0
+ *
+ * Warnings:
+ *
+ * Block 1, CTA-861 Extension Block:
+ * IT Video Formats are overscanned by default, but normally this should be underscanned.
+ *
+ * EDID conformity: PASS
+ */
+const unsigned char test_edid_hdmi_qms_only[] = {
+ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x31, 0xd8, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
+ 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x20,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x04, 0x74, 0x80, 0x18, 0x71, 0x38,
+ 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
+ 0x04, 0x74, 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, 0x8a, 0x00,
+ 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x54,
+ 0x65, 0x73, 0x74, 0x20, 0x45, 0x44, 0x49, 0x44, 0x0a, 0x20, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x78, 0x18, 0x87, 0x22, 0x00, 0x0a,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x7c, 0x02, 0x03, 0x29, 0x31,
+ 0x42, 0x3f, 0x5f, 0x6d, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x78, 0x00, 0x20,
+ 0x00, 0x00, 0x00, 0x20, 0x61, 0x6d, 0xd8, 0x5d, 0xc4, 0x01, 0x00, 0x80,
+ 0x07, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0xe2, 0x0e, 0x61, 0xe2, 0x00,
+ 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xbe
+};
+EXPORT_SYMBOL(test_edid_hdmi_qms_only);
+
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>");
MODULE_DESCRIPTION("EDID Definitions for KUnit tests");
MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tests/drm_kunit_edid.h b/drivers/gpu/drm/tests/drm_kunit_edid.h
index 80dc15a8bcb6..e993bbd5427b 100644
--- a/drivers/gpu/drm/tests/drm_kunit_edid.h
+++ b/drivers/gpu/drm/tests/drm_kunit_edid.h
@@ -13,5 +13,7 @@ extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_1080p_rgb_yuv_4k_yuv420_dc_max_200mhz[256];
extern const unsigned char test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz[256];
extern const unsigned char test_edid_hdmi_vrr[256];
+extern const unsigned char test_edid_hdmi_vrr_qms[256];
+extern const unsigned char test_edid_hdmi_qms_only[256];
#endif // DRM_KUNIT_EDID_H_
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 25/27] drm/connector: Fix docs for cnmvrr and cinemavrr members
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
The member names in the doc comments are inexplicably prefixed with an
"m", which does not reflect the actual struct member's name.
Drop the "m".
Fixes: 014da1db5e64 ("drm/edid: parse HDMI 2.1 gaming (ALLM/VRR) capabilities from HF-VSDB")
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
include/drm/drm_connector.h | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 41f272afbb1e..021ab07a1d9b 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -265,10 +265,10 @@ struct drm_hdmi_vrr_cap {
/** @fva: flag for Fast VActive (Quick Frame Transport) support */
bool fva;
- /** @mcnmvrr: flag for Negative M VRR support */
+ /** @cnmvrr: flag for Negative M VRR support */
bool cnmvrr;
- /** @mcinema_vrr: flag for Cinema VRR support */
+ /** @cinema_vrr: flag for Cinema VRR support */
bool cinema_vrr;
/** @mdelta: flag for limited frame-to-frame compensation support */
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 25/27] drm/connector: Fix docs for cnmvrr and cinemavrr members
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
The member names in the doc comments are inexplicably prefixed with an
"m", which does not reflect the actual struct member's name.
Drop the "m".
Fixes: 014da1db5e64 ("drm/edid: parse HDMI 2.1 gaming (ALLM/VRR) capabilities from HF-VSDB")
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
include/drm/drm_connector.h | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index 41f272afbb1e..021ab07a1d9b 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -265,10 +265,10 @@ struct drm_hdmi_vrr_cap {
/** @fva: flag for Fast VActive (Quick Frame Transport) support */
bool fva;
- /** @mcnmvrr: flag for Negative M VRR support */
+ /** @cnmvrr: flag for Negative M VRR support */
bool cnmvrr;
- /** @mcinema_vrr: flag for Cinema VRR support */
+ /** @cinema_vrr: flag for Cinema VRR support */
bool cinema_vrr;
/** @mdelta: flag for limited frame-to-frame compensation support */
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 26/27] drm/debugfs: Expose HDMI VRR in vrr_range debugfs
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
IGT requires the vrr_range debugfs file for running the tests, for
better or worse.
Since Commit 014da1db5e64 ("drm/edid: parse HDMI 2.1 gaming (ALLM/VRR)
capabilities from HF-VSDB") parses the HDMI VRR minimum and maximum
numbers into separate members and not the display_info.monitor_range
member, this debugfs entry needs to use those members when the display
is HDMI and does HDMI VRR.
Add this special casing, and also protect accesses to connector->status
and connector->display_info interruptibly with the mode_config mutex.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_debugfs.c | 24 +++++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/drm_debugfs.c b/drivers/gpu/drm/drm_debugfs.c
index a72f9723d3a9..476f44642ac3 100644
--- a/drivers/gpu/drm/drm_debugfs.c
+++ b/drivers/gpu/drm/drm_debugfs.c
@@ -586,12 +586,26 @@ static ssize_t edid_write(struct file *file, const char __user *ubuf,
static int vrr_range_show(struct seq_file *m, void *data)
{
struct drm_connector *connector = m->private;
+ u16 min_vfreq, max_vfreq;
+
+ scoped_cond_guard(mutex_intr, return -EINTR,
+ &connector->dev->mode_config.mutex) {
+ struct drm_display_info *info = &connector->display_info;
+
+ if (connector->status != connector_status_connected)
+ return -ENODEV;
+
+ if (info->is_hdmi && info->hdmi.vrr_cap.supported) {
+ min_vfreq = info->hdmi.vrr_cap.vrr_min;
+ max_vfreq = info->hdmi.vrr_cap.vrr_max;
+ } else {
+ min_vfreq = info->monitor_range.min_vfreq;
+ max_vfreq = info->monitor_range.max_vfreq;
+ }
+ }
- if (connector->status != connector_status_connected)
- return -ENODEV;
-
- seq_printf(m, "Min: %u\n", connector->display_info.monitor_range.min_vfreq);
- seq_printf(m, "Max: %u\n", connector->display_info.monitor_range.max_vfreq);
+ seq_printf(m, "Min: %u\n", min_vfreq);
+ seq_printf(m, "Max: %u\n", max_vfreq);
return 0;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 26/27] drm/debugfs: Expose HDMI VRR in vrr_range debugfs
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
IGT requires the vrr_range debugfs file for running the tests, for
better or worse.
Since Commit 014da1db5e64 ("drm/edid: parse HDMI 2.1 gaming (ALLM/VRR)
capabilities from HF-VSDB") parses the HDMI VRR minimum and maximum
numbers into separate members and not the display_info.monitor_range
member, this debugfs entry needs to use those members when the display
is HDMI and does HDMI VRR.
Add this special casing, and also protect accesses to connector->status
and connector->display_info interruptibly with the mode_config mutex.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/drm_debugfs.c | 24 +++++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/drm_debugfs.c b/drivers/gpu/drm/drm_debugfs.c
index a72f9723d3a9..476f44642ac3 100644
--- a/drivers/gpu/drm/drm_debugfs.c
+++ b/drivers/gpu/drm/drm_debugfs.c
@@ -586,12 +586,26 @@ static ssize_t edid_write(struct file *file, const char __user *ubuf,
static int vrr_range_show(struct seq_file *m, void *data)
{
struct drm_connector *connector = m->private;
+ u16 min_vfreq, max_vfreq;
+
+ scoped_cond_guard(mutex_intr, return -EINTR,
+ &connector->dev->mode_config.mutex) {
+ struct drm_display_info *info = &connector->display_info;
+
+ if (connector->status != connector_status_connected)
+ return -ENODEV;
+
+ if (info->is_hdmi && info->hdmi.vrr_cap.supported) {
+ min_vfreq = info->hdmi.vrr_cap.vrr_min;
+ max_vfreq = info->hdmi.vrr_cap.vrr_max;
+ } else {
+ min_vfreq = info->monitor_range.min_vfreq;
+ max_vfreq = info->monitor_range.max_vfreq;
+ }
+ }
- if (connector->status != connector_status_connected)
- return -ENODEV;
-
- seq_printf(m, "Min: %u\n", connector->display_info.monitor_range.min_vfreq);
- seq_printf(m, "Max: %u\n", connector->display_info.monitor_range.max_vfreq);
+ seq_printf(m, "Min: %u\n", min_vfreq);
+ seq_printf(m, "Max: %u\n", max_vfreq);
return 0;
}
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 27/27] drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
2026-11-05 15:51 ` Nicolas Frattaroli
@ 2026-11-05 15:52 ` Nicolas Frattaroli
-1 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Attach the vrr_display_min and vrr_display_max properties to the KUnit
HDMI state helper test connector, and add two simple tests to verify the
introduced drm_connector_hdmi_update_vrr_limits() helper sets the
properties right depending on EDID.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 87 ++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 4c7e563ca51d..7bd06b65d241 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -308,6 +308,9 @@ __connector_hdmi_init(struct kunit *test,
KUNIT_ASSERT_EQ(test, ret, 0);
conn->hdmi.qms_supported = true;
+ ret = drm_connector_attach_vrr_display_limits_properties(conn);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
drm_mode_config_reset(drm);
if (edid_data && edid_len) {
@@ -3676,6 +3679,88 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
crtc_state->vrr_state.max_vtotal);
}
+/*
+ * Check that on a VRR-capable sink, the HDMI VRR limits helper sets the
+ * display_min/display_max connector properties to the right values.
+ */
+static void drm_test_check_hdmi_vrr_display_limits(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_display_mode *preferred;
+ struct drm_connector *conn;
+ u64 disp_min, disp_max;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
+ &disp_min);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
+ &disp_max);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, disp_min, 30);
+ KUNIT_EXPECT_EQ(test, disp_max, 120);
+}
+
+/*
+ * Check that on a non-VRR-capable sink, the HDMI VRR limits helper sets the
+ * display_min/display_max connector properties to 0.
+ */
+static void drm_test_check_hdmi_vrr_display_limits_zero(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_display_mode *preferred;
+ struct drm_connector *conn;
+ u64 disp_min, disp_max;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
+ &disp_min);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
+ &disp_max);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, disp_min, 0);
+ KUNIT_EXPECT_EQ(test, disp_max, 0);
+}
+
/*
* Check that on a VRR-capable sink with VRR enabled and a fixed target rate
* set, an atomic commit works, and the derived vtotals are as expected.
@@ -4622,6 +4707,8 @@ static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_display_limits),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_display_limits_zero),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_flush_vsync),
KUNIT_CASE_PARAM(drm_test_check_hdmi_vrr_target_rate_fail,
--
2.56.0
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v2 27/27] drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
@ 2026-11-05 15:52 ` Nicolas Frattaroli
0 siblings, 0 replies; 74+ messages in thread
From: Nicolas Frattaroli @ 2026-11-05 15:52 UTC (permalink / raw)
To: Xaver Hugl, Vidith Madhu, Borah, Chaitanya Kumar, Leo Li,
Harry Wentland, Fangzhi Zuo, Daniel Stone, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Helge Deller, Andrzej Hajda, Neil Armstrong, Robert Foss,
Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
Sandy Huang, Heiko Stübner, Andy Yan, Michel Dänzer
Cc: dri-devel, linux-kernel, linux-fbdev, linux-rockchip,
linux-arm-kernel, kernel, Derek Foreman, wayland-devel,
Nicolas Frattaroli
Attach the vrr_display_min and vrr_display_max properties to the KUnit
HDMI state helper test connector, and add two simple tests to verify the
introduced drm_connector_hdmi_update_vrr_limits() helper sets the
properties right depending on EDID.
Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c | 87 ++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
index 4c7e563ca51d..7bd06b65d241 100644
--- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
+++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
@@ -308,6 +308,9 @@ __connector_hdmi_init(struct kunit *test,
KUNIT_ASSERT_EQ(test, ret, 0);
conn->hdmi.qms_supported = true;
+ ret = drm_connector_attach_vrr_display_limits_properties(conn);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
drm_mode_config_reset(drm);
if (edid_data && edid_len) {
@@ -3676,6 +3679,88 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
crtc_state->vrr_state.max_vtotal);
}
+/*
+ * Check that on a VRR-capable sink, the HDMI VRR limits helper sets the
+ * display_min/display_max connector properties to the right values.
+ */
+static void drm_test_check_hdmi_vrr_display_limits(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_display_mode *preferred;
+ struct drm_connector *conn;
+ u64 disp_min, disp_max;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_vrr);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
+ &disp_min);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
+ &disp_max);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, disp_min, 30);
+ KUNIT_EXPECT_EQ(test, disp_max, 120);
+}
+
+/*
+ * Check that on a non-VRR-capable sink, the HDMI VRR limits helper sets the
+ * display_min/display_max connector properties to 0.
+ */
+static void drm_test_check_hdmi_vrr_display_limits_zero(struct kunit *test)
+{
+ struct drm_atomic_helper_connector_hdmi_priv *priv;
+ struct drm_display_mode *preferred;
+ struct drm_connector *conn;
+ u64 disp_min, disp_max;
+ int ret;
+
+ priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
+ BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
+ 8,
+ &dummy_connector_hdmi_funcs,
+ test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
+ KUNIT_ASSERT_NOT_NULL(test, priv);
+
+ conn = &priv->connector;
+ KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
+ KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
+
+ preferred = find_preferred_mode(conn);
+ KUNIT_ASSERT_NOT_NULL(test, preferred);
+
+ ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
+ &disp_min);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
+ &disp_max);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, disp_min, 0);
+ KUNIT_EXPECT_EQ(test, disp_max, 0);
+}
+
/*
* Check that on a VRR-capable sink with VRR enabled and a fixed target rate
* set, an atomic commit works, and the derived vtotals are as expected.
@@ -4622,6 +4707,8 @@ static struct kunit_case drm_atomic_helper_connector_hdmi_vrr_tests[] = {
KUNIT_CASE(drm_test_check_hdmi_vrr),
KUNIT_CASE(drm_test_check_hdmi_vrr_sink_fail),
KUNIT_CASE(drm_test_check_hdmi_vrr_flush_vsync),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_display_limits),
+ KUNIT_CASE(drm_test_check_hdmi_vrr_display_limits_zero),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate),
KUNIT_CASE(drm_test_check_hdmi_vrr_fixed_rate_flush_vsync),
KUNIT_CASE_PARAM(drm_test_check_hdmi_vrr_target_rate_fail,
--
2.56.0
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
^ permalink raw reply related [flat|nested] 74+ messages in thread
* Re: [PATCH v2 02/27] drm/connector: add vrr_display_mode connector property
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:27 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:27 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] drm/connector: missing kernel-doc header on enum vrr_display_mode
--
commit da4673cb9da04a6d510c119f2c09b6e0b6e956b2
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/connector: add vrr_display_mode connector property
This commit introduces the vrr_display_mode connector enum property and its
associated connector state field so userspace can explicitly control whether
a VRR-capable display operates in Game or Off mode regardless of CRTC state.
> diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
> index 2ee7c59329aae..8699f35cafcba 100644
> --- a/include/drm/drm_connector.h
> +++ b/include/drm/drm_connector.h
> @@ -809,6 +809,15 @@ enum drm_bus_flags {
> DRM_BUS_FLAG_SHARP_SIGNALS = BIT(8),
> };
>
> +enum vrr_display_mode {
> + /** @VRR_DISPLAY_MODE_GAME: Set the display to VRR "Game" mode */
> + VRR_DISPLAY_MODE_GAME = 0,
> + /** @VRR_DISPLAY_MODE_OFF: Turn off VRR on the display */
> + VRR_DISPLAY_MODE_OFF,
> + /** @VRR_DISPLAY_MODE_COUNT: Sentinel, not a valid mode, keep last */
> + VRR_DISPLAY_MODE_COUNT
> +};
[Severity: Low]
enum vrr_display_mode documents its enumerators with inline kernel-doc
comments and is cross-referenced as &enum vrr_display_mode from
struct drm_connector_state and struct drm_connector, but it does not have an
opening kernel-doc comment block above the enum declaration.
Because kernel-doc only enters declaration parsing after encountering a
standalone /** header line, scripts/kernel-doc skips enum vrr_display_mode
and its inline member descriptions when processing include/drm/drm_connector.h.
Should a /** enum vrr_display_mode - ... */ kernel-doc header block be added
above enum vrr_display_mode?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=2
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 03/27] video/hdmi: Add VTEM EMP packing
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:29 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:29 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] video/hdmi: hdmi_infoframe_pack_only() missing EMP support
- [Medium] video/hdmi: missing bounds and type checks in hdmi_emp_infoframe_vtem_pack()
- [Low] video/hdmi: missing @vtem kernel-doc entry in union hdmi_infoframe
--
commit 435f43f92e62fa2ad37ec03ba43478dd2a0e2bba
Author: Derek Foreman <derek.foreman@collabora.com>
video/hdmi: Add VTEM EMP packing
This commit adds structures and helpers to initialize, pack, and log HDMI
Video Timing Extended Metadata (VTEM) Extended Metadata Packets (EMPs) for
Variable Refresh Rate support.
> diff --git a/drivers/video/hdmi.c b/drivers/video/hdmi.c
> index 45b42f14a7503..ed9abc7ad2285 100644
> --- a/drivers/video/hdmi.c
> +++ b/drivers/video/hdmi.c
> @@ -983,6 +983,10 @@ hdmi_infoframe_pack(union hdmi_infoframe *frame,
> length = hdmi_vendor_any_infoframe_pack(&frame->vendor,
> buffer, size);
> break;
> + case HDMI_INFOFRAME_TYPE_EMP:
> + length = hdmi_emp_infoframe_vtem_pack(&frame->vtem,
> + buffer, size);
> + break;
> default:
> WARN(1, "Bad infoframe type %d\n", frame->any.type);
> length = -EINVAL;
[Severity: Medium]
As reported by Sashiko on RFC [1], hdmi_infoframe_pack() handles
HDMI_INFOFRAME_TYPE_EMP, but hdmi_infoframe_pack_only() in
drivers/video/hdmi.c was not updated to handle it:
drivers/video/hdmi.c:hdmi_infoframe_pack_only() {
switch (frame->any.type) {
case HDMI_INFOFRAME_TYPE_AVI:
...
case HDMI_INFOFRAME_TYPE_VENDOR:
length = hdmi_vendor_any_infoframe_pack_only(&frame->vendor,
buffer, size);
break;
default:
WARN(1, "Bad infoframe type %d\n", frame->any.type);
length = -EINVAL;
}
}
If a driver packs an EMP infoframe stored in union hdmi_infoframe via
hdmi_infoframe_pack_only(), won't it hit the default branch, trigger the
WARN(1), and fail with -EINVAL?
Also, since hdmi_emp_infoframe_vtem_pack() does not modify its vtem
argument, should its parameter be const struct hdmi_emp_infoframe_vtem *
(or have a corresponding _pack_only helper) so hdmi_infoframe_pack_only()
can call it with a const union hdmi_infoframe *?
[ ... ]
> @@ -1919,3 +1951,66 @@ int hdmi_infoframe_unpack(union hdmi_infoframe *frame,
> return ret;
> }
> EXPORT_SYMBOL(hdmi_infoframe_unpack);
> +
> +/**
> + * hdmi_emp_infoframe_vtem_init() - initialize an HDMI VTEM EMP
> + * @emp: HDMI VTEM EMP
> + */
> +void hdmi_emp_infoframe_vtem_init(struct hdmi_emp_infoframe_vtem *emp)
> +{
> + memset(emp, 0, sizeof(*emp));
> + emp->type = HDMI_INFOFRAME_TYPE_EMP;
> +}
> +EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_init);
> +
> +/**
> + * hdmi_emp_infoframe_vtem_pack() - pack a vtem into a binary buffer
> + * @vtem: pointer to &struct hdmi_emp_infoframe_vtem to pack
> + * @buffer: pointer to pre-allocated output buffer
> + * @size: maximum size in bytes of @buffer
> + *
> + * Pack a VTEM infoframe struct into a binary buffer in a way that's suitable
> + * to be sent out over the wire.
> + *
> + * Returns the number of bytes packed into the binary buffer or a negative
> + * error code on failure.
> + */
> +ssize_t hdmi_emp_infoframe_vtem_pack(struct hdmi_emp_infoframe_vtem *vtem,
> + void *buffer, size_t size)
> +{
> + u8 *ptr = buffer;
> +
> + if (size < HDMI_INFOFRAME_SIZE(VTEM))
> + return -EINVAL;
> +
> + /* 4 bits max */
> + if (vtem->fva_factor_m1 > 0xF)
> + return -EINVAL;
> +
> + memset(buffer, 0, size);
> +
> + /* Header Data */
> + ptr[0] = vtem->type;
> + ptr[1] = BIT(7) | BIT(6);
> + ptr[2] = 0;
> +
> + ptr[3] = BIT(7) | BIT(2);
> +
> + ptr[4] = 0;
> + ptr[5] = 1;
> + ptr[6] = 0;
> + ptr[7] = 1;
> + ptr[8] = 0;
> + ptr[9] = 4;
> +
> + /* This is where the VTEM part starts */
> + ptr[10] = (vtem->fva_factor_m1 << 4) |
> + (vtem->m_const ? BIT(1) : 0) |
> + (vtem->game_vrr_en ? BIT(0) : 0);
> + ptr[11] = vtem->base_vfront;
> + ptr[12] = vtem->base_refresh_rate >> 8 & 0x3;
> + ptr[13] = vtem->base_refresh_rate & 0xFF;
> +
> + return 14;
> +}
> +EXPORT_SYMBOL(hdmi_emp_infoframe_vtem_pack);
[Severity: Medium]
As reported by Sashiko on RFC [2], hdmi_emp_infoframe_vtem_pack() checks
that vtem->fva_factor_m1 fits in 4 bits and returns -EINVAL if exceeded,
but silently masks the u16 vtem->base_refresh_rate to 10 bits when writing
ptr[12] and ptr[13] instead of rejecting values above 0x3ff. It also
writes ptr[0] = vtem->type without verifying that vtem->type is
HDMI_INFOFRAME_TYPE_EMP.
In the DRM HDMI state helper path added later in this series,
drm_atomic_helper_connector_hdmi_check() calls hdmi_validate_vrr(), which
returns early when new_crtc_state->vrr_enabled is false and skips its
refresh rate check, while hdmi_generate_emp_infoframe_vtem() still runs
and sets vtem->base_refresh_rate from drm_mode_vrefresh(&crtc_state->mode)
for non-VIC modes before calling hdmi_infoframe_pack().
Should hdmi_emp_infoframe_vtem_pack() validate that
vtem->base_refresh_rate <= 0x3ff and vtem->type == HDMI_INFOFRAME_TYPE_EMP,
returning -EINVAL on invalid input rather than transmitting a truncated or
malformed packet?
> diff --git a/include/linux/hdmi.h b/include/linux/hdmi.h
> index b80a5ee63bb23..71e2b36896afe 100644
> --- a/include/linux/hdmi.h
> +++ b/include/linux/hdmi.h
[ ... ]
> @@ -457,6 +475,7 @@ union hdmi_infoframe {
> union hdmi_vendor_any_infoframe vendor;
> struct hdmi_audio_infoframe audio;
> struct hdmi_drm_infoframe drm;
> + struct hdmi_emp_infoframe_vtem vtem;
> };
[Severity: Low]
This isn't a runtime bug, but union hdmi_infoframe in include/linux/hdmi.h
has a kernel-doc comment block above it (included by
Documentation/gpu/drm-kms-helpers.rst):
include/linux/hdmi.h:
/**
* union hdmi_infoframe - overall union of all abstract infoframe representations
* @any: generic infoframe
* @avi: avi infoframe
* @spd: spd infoframe
* @vendor: union of all vendor infoframes
* @audio: audio infoframe
* @drm: Dynamic Range and Mastering infoframe
Should an @vtem entry be added to the kernel-doc comment for union
hdmi_infoframe to avoid a scripts/kernel-doc warning during W=1 or
htmldocs builds?
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-4-2fcd7d011646@collabora.com
[2] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-4-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=3
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 04/27] drm/bridge: Add VTEM EMP support
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:33 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:33 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] drm/bridge: wrong kernel-doc tag for hdmi_write_vtem_emp in drm_bridge_funcs
--
commit db5205bd90d10ddf734b146e8cd3c0d61b65f621
Author: Derek Foreman <derek.foreman@collabora.com>
drm/bridge: Add VTEM EMP support
This commit adds VTEM EMP callbacks, the DRM_BRIDGE_OP_HDMI_VTEM_EMP
operation flag, and corresponding drm_bridge_connector wiring to support
VTEM extended metadata packets on HDMI bridges.
> diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h
> index 1981d24a700d2..6913662023cae 100644
> --- a/include/drm/drm_bridge.h
> +++ b/include/drm_bridge.h
> @@ -684,6 +684,29 @@ struct drm_bridge_funcs {
> int (*hdmi_write_spd_infoframe)(struct drm_bridge *bridge,
> const u8 *buffer, size_t len);
>
> + /**
> + * @hdmi_clear_vtem_emp:
> + *
> + * This callback clears the emp in the hardware during commit.
> + *
> + * This callback is optional but it must be implemented by bridges that
> + * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
> + * &drm_bridge->ops.
> + */
> + int (*hdmi_clear_vtem_emp)(struct drm_bridge *bridge);
> +
> + /**
> + * @hdmi_write_spd_infoframe:
> + *
> + * Program the emp into the hardware.
> + *
> + * This callback is optional but it must be implemented by bridges that
> + * set the DRM_BRIDGE_OP_HDMI_VTEM_EMP flag in their
> + * &drm_bridge->ops.
> + */
> + int (*hdmi_write_vtem_emp)(struct drm_bridge *bridge,
> + const u8 *buffer, size_t len);
[Severity: Low]
As reported by Sashiko on RFC [1], should the kernel-doc comment for
hdmi_write_vtem_emp in struct drm_bridge_funcs in include/drm/drm_bridge.h
use @hdmi_write_vtem_emp: instead of @hdmi_write_spd_infoframe:, so that
running scripts/kernel-doc does not warn about a duplicate
@hdmi_write_spd_infoframe section and an undescribed hdmi_write_vtem_emp
member?
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-5-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=4
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 01/27] drm: Add VRR state
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:44 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:44 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] drm/hdmi: division by zero in hdmi_calculate_vtotal() on zero denom
- [High] drm/hdmi: hdmi_validate_vrr() rejects modes with refresh below vrr_min
- [High] drm/hdmi: invalid VRR error returns and stale state in hdmi_validate_vrr()
- [High] drm/hdmi: bypassed VRR validation and stale vrr_state in hdmi_validate_vrr()
- [Medium] drm/hdmi: u64 quotient truncated to u32 in hdmi_calculate_vtotal()
- [Medium] drm/hdmi: rounded mode_refresh in hdmi_validate_vrr() inflates base_vtotal
- [Medium] drm/hdmi: hdmi_validate_vrr() skipped when CRTC VRR state changes alone
- [Low] drm/hdmi: wrong numerator variable in hdmi_calculate_vtotal() comment
--
commit e5085c76b79fa8bd381ce1216875922c59e1c24a
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm: Add VRR state
This commit adds struct drm_crtc_vrr_state to struct drm_crtc_state and
implements HDMI VRR state validation and vtotal calculation in
drm_hdmi_state_helper.c.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> index 7cdb7ca3dc12f..c1a84e30441a6 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> @@ -887,6 +887,210 @@ hdmi_generate_infoframes(const struct drm_connector *connector,
[ ... ]
> +static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
> + u32 rate_n, u32 rate_d, u16 *out_vtotal,
> + u32 *out_denom, u32 *out_frame_err)
> +{
> + u32 denom, result, err;
> + u64 numerator;
> +
> + /*
[ ... ]
> + * clock
> + * Vtotal = ----------------------------------
> + * Htotal * target_n / target_d
> + *
> + * Some algebra brings us to:
> + *
> + * clock * target_n
> + * Vtotal = ----------------------------------
> + * Htotal * rate_n
[Severity: Low]
This isn't a bug since the code below multiplies mode->crtc_clock by rate_d,
but shouldn't the numerator in this comment be clock * target_d (or
clock * rate_d) rather than clock * target_n?
> + *
> + * The amount of timing error we accumulate every frame will be
> + * the remainder of that division.
[ ... ]
> + */
> +
> + if (check_mul_overflow(mode->crtc_htotal, rate_n, &denom))
> + return -ERANGE;
> +
> + if (check_mul_overflow(mode->crtc_clock * 1000ULL, rate_d, &numerator))
> + return -ERANGE;
> +
> + result = div_u64_rem(numerator, denom, &err);
> + if (result > U16_MAX)
> + return -ERANGE;
[Severity: High]
Can div_u64_rem() divide by zero here when denom is 0?
check_mul_overflow(mode->crtc_htotal, rate_n, &denom) returns false and sets
denom = 0 whenever mode->crtc_htotal == 0 or rate_n == 0, and div_u64_rem()
is then called without checking !denom.
In drm_atomic_helper_connector_hdmi_check() -> hdmi_validate_vrr(), mode is
taken from &new_crtc_state->mode during connector atomic_check before
mode_fixup() has run. If an in-kernel KMS caller or KUnit test sets a mode on
a CRTC via drm_atomic_set_mode_for_crtc() that was constructed without
drm_mode_set_crtcinfo() (such as via drm_display_mode_from_cea_vic(),
drm_kunit_display_mode_from_cea_vic(), DRM_MODE(), or DRM_MODE_INIT()),
mode->htotal and mode->vtotal are populated so drm_mode_vrefresh(mode)
succeeds, vfront (mode->crtc_vsync_start - mode->crtc_vdisplay) is 0, and
hdmi_calculate_vtotal() is reached with mode->crtc_htotal == 0.
Similarly, drm_mode_vrefresh() returns 0 (never negative) on invalid timings
or overflow, and hdmi_validate_vrr() only rejects mode_refresh < 0 ||
mode_refresh > 1023, so mode_refresh == 0 can also clamp max_vfreq_n to 0 and
pass rate_n == 0 into hdmi_calculate_vtotal().
[Severity: Medium]
Also in hdmi_calculate_vtotal(), since result is declared as u32 while
numerator is u64, doesn't assigning the return value of div_u64_rem() to
result truncate the upper 32 bits before checking result > U16_MAX (and raise
a divide error #DE in do_div() on 32-bit x86 if the quotient exceeds
U32_MAX)?
For example, a custom mode passed via DRM_IOCTL_MODE_ATOMIC with a large
clock and vscan > 1 (which reduces drm_mode_vrefresh(mode) without increasing
mode->crtc_htotal) can keep drm_mode_vrefresh(mode) <= 1023 while producing a
quotient in hdmi_calculate_vtotal() such as 0x10000040dULL, which truncates
to u32 result = 0x40d and bypasses the result > U16_MAX check. Should result
be u64 (or div64_u64_rem())?
> +
> + if (out_vtotal)
> + *out_vtotal = result;
> + if (out_denom)
> + *out_denom = denom;
> + if (out_frame_err)
> + *out_frame_err = err;
> +
> + return 0;
> +}
> +
> +static int hdmi_validate_vrr(struct drm_connector *connector,
> + struct drm_atomic_commit *state)
> +{
> + struct drm_connector_state *new_conn_state =
> + drm_atomic_get_new_connector_state(state, connector);
> + struct drm_crtc_state *new_crtc_state =
> + drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
> + struct drm_crtc_vrr_state *vrr_state =
> + &new_crtc_state->vrr_state;
> + struct drm_display_info *info = &connector->display_info;
> + const struct drm_display_mode *mode = &new_crtc_state->mode;
> + struct drm_device *dev = connector->dev;
> + int mode_refresh, vfront, ret;
> + u32 min_vfreq_n = info->hdmi.vrr_cap.vrr_min;
> + u32 min_vfreq_d = 1;
> + u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
> + u32 max_vfreq_d = 1;
> +
> + if (!new_crtc_state->vrr_enabled)
> + return 0;
> +
> + if (!info->hdmi.vrr_cap.supported)
> + return -EOPNOTSUPP;
[Severity: High]
Does returning -EOPNOTSUPP here (and -EINVAL below when the mode or sink
cannot do HDMI VRR) break the DRM UAPI contract for VRR_ENABLED, and does
returning 0 when !new_crtc_state->vrr_enabled leave stale values in
new_crtc_state->vrr_state?
1. The documentation for drm_crtc_state.vrr_enabled in include/drm/drm_crtc.h
states:
/*
* Indicates if variable refresh rate should be enabled for the CRTC.
* Support for the requested vrr state will depend on driver and
* hardware capabiltiy - lacking support is not treated as failure.
*/
Every atomic CRTC attaches VRR_ENABLED in drm_crtc_init_with_planes(), and
drm_atomic_helper_connector_hdmi_check() unconditionally calls
hdmi_validate_vrr(). Because __drm_atomic_helper_set_config() and
drm_client_modeset_commit_atomic() preserve crtc->state->vrr_enabled across
modesets, hotplugging a non-VRR HDMI sink while VRR_ENABLED is set causes
subsequent fbdev/drm_client console restores and legacy SETCRTC calls to fail
with -EOPNOTSUPP. It also rejects commits on HDMI drivers that use
drm_atomic_helper_connector_hdmi_check() without supporting VRR, as well as
connectors set to VRR_DISPLAY_MODE_OFF in later commit "drm/connector: add
vrr_display_mode connector property".
2. When new_crtc_state->vrr_enabled transitions from true to false,
__drm_atomic_helper_crtc_duplicate_state() copies the previous
crtc->state->vrr_state via memcpy(). Returning 0 immediately at
if (!new_crtc_state->vrr_enabled) without clearing new_crtc_state->vrr_state
leaves base_vtotal, max_vtotal, cur_vtotal, and dynamic populated. In later
commit "drm/connector: hdmi: Add VTEM EMP generation",
hdmi_generate_emp_infoframe_vtem() sets vtem->m_const = !vrr->dynamic, so
leaving vrr->dynamic as true transmits M_CONST = 0 when VRR is disabled. In
addition, in later commit "drm/rockchip: vop2: Enable VRR" (with
"drm/crtc-helper: Add VRR helper functions"), vop2_crtc_atomic_flush() only
calls drm_crtc_helper_vrr_flush() when crtc_state->vrr_enabled is true; since
cur_vtotal was not cleared when VRR was disabled, re-enabling VRR at the same
base_vtotal sees cur_vtotal == base_vtotal and skips reprogramming the
hardware VTOTAL register.
> +
> + mode_refresh = drm_mode_vrefresh(mode);
> +
> + /* HDMI uses 10 bits to signal the base refresh. */\
> + if (mode_refresh < 0 || mode_refresh > 1023) {
> + drm_dbg_kms(dev, "Mode's refresh of %dHz > HDMI VRR maximum (1023)\n",
> + mode_refresh);
> + return -EINVAL;
> + }
> +
> + if (!max_vfreq_n || max_vfreq_n > mode_refresh)
> + max_vfreq_n = mode_refresh;
> +
> + if (!min_vfreq_n) {
> + drm_dbg_kms(dev, "Could not get minimum VRR rate from sink\n");
> + return -EINVAL;
> + }
> +
> + vfront = mode->crtc_vsync_start - mode->crtc_vdisplay;
> + if (vfront < 0 || vfront > U8_MAX) {
> + drm_dbg_kms(dev, "Vfront of %d would not fit in VTEM packet\n", vfront);
> + return -EINVAL;
> + }
> +
> + if (cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0) {
> + drm_dbg_kms(dev, "Target max (%u/%u) > target min (%u/%u)\n",
> + max_vfreq_n, max_vfreq_d, min_vfreq_n, min_vfreq_d);
> + return -EINVAL;
> + }
[Severity: High]
Doesn't clamping max_vfreq_n to mode_refresh before this cmp_fraction() check
cause hdmi_validate_vrr() to reject any mode whose refresh rate is below the
sink's vrr_min (such as 24Hz, 25Hz, or 30Hz modes on a 48-120Hz HDMI 2.1 TV)?
Because max_vfreq_n is clamped to at most mode_refresh above, whenever
mode_refresh < min_vfreq_n we have min_vfreq_n > max_vfreq_n, so
cmp_fraction(min_vfreq_n, min_vfreq_d, max_vfreq_n, max_vfreq_d) > 0 is true
and returns -EINVAL (while also logging an inverted debug message claiming
Target max > target min). That also makes the fallback branch further down:
if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
/* refresh <= VRR min, don't do VRR vtotal adjustment */
vrr_state->max_vtotal = mode->crtc_vtotal;
}
unreachable whenever mode_refresh < min_vfreq_n.
> +
> + ret = hdmi_calculate_vtotal(mode, max_vfreq_n, max_vfreq_d,
> + &vrr_state->base_vtotal, NULL, NULL);
> + if (ret) {
> + drm_dbg_kms(dev, "Couldn't calculate base_vtotal: %pe\n", ERR_PTR(ret));
> + return ret;
> + }
> +
> + /*
> + * Even at its fastest, it can't go faster than the mode, so
> + * clamp to avoid imprecisely rounded mode_refresh values we
> + * can't do anything about from ruining our day.
> + */
> + vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
> +
> + if (cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0) {
> + /* refresh <= VRR min, don't do VRR vtotal adjustment */
> + vrr_state->max_vtotal = mode->crtc_vtotal;
> + } else {
[Severity: Medium]
What happens here when drm_mode_vrefresh(mode) rounds down a mode's
fractional refresh rate?
When !max_vfreq_n || max_vfreq_n > mode_refresh, max_vfreq_n is set to the
integer-rounded mode_refresh from drm_mode_vrefresh(mode) (which uses
DIV_ROUND_CLOSEST_ULL). If the mode's actual refresh rate has a fractional
part in (0, 0.5) Hz (for example, 800x600@60.317Hz with clock = 40000,
htotal = 1056, vtotal = 628 where drm_mode_vrefresh() rounds down to 60, or a
48.4Hz mode with clock = 48400, htotal = 1000, vtotal = 1000 where
drm_mode_vrefresh() rounds down to 48), hdmi_calculate_vtotal() divides by
the smaller rounded rate and computes vrr_state->base_vtotal >
mode->crtc_vtotal (631 > 628, or 1008 > 1000).
Clamping with max(vrr_state->base_vtotal, mode->crtc_vtotal) only guards
against mode_refresh rounding up, not rounding down. Moreover, if
min_vfreq_n == mode_refresh (e.g. vrr_min = 48 on the 48.4Hz mode), the
cmp_fraction(min_vfreq_n, min_vfreq_d, mode_refresh, 1) >= 0 branch sets
vrr_state->max_vtotal = mode->crtc_vtotal (1000), leaving
vrr_state->base_vtotal (1008) > vrr_state->max_vtotal (1000).
When max_vfreq is not capped below the mode's refresh rate, shouldn't
vrr_state->base_vtotal simply be mode->crtc_vtotal?
[ ... ]
> @@ -919,6 +1123,10 @@ int drm_atomic_helper_connector_hdmi_check(struct drm_connector *connector,
>
> new_conn_state->hdmi.is_limited_range = hdmi_is_limited_range(connector, new_conn_state);
>
> + ret = hdmi_validate_vrr(connector, state);
> + if (ret)
> + return ret;
[Severity: High]
Is calling hdmi_validate_vrr() only from
drm_atomic_helper_connector_hdmi_check() sufficient when userspace updates
CRTC VRR properties without a modeset or connector property change?
VRR_ENABLED (as well as the VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR} properties
added later in commit "drm: Add VRR target frame rate properties") are CRTC
properties stored on struct drm_crtc_state. In
drm_atomic_helper_check_modeset(), drm_atomic_add_affected_connectors() is
only called when drm_atomic_crtc_needs_modeset() is true. If userspace
submits a DRM_IOCTL_MODE_ATOMIC commit that changes VRR properties on an
active CRTC without changing MODE_ID, ACTIVE, or any connector property, the
connector is not added to state->connectors and
drm_atomic_helper_connector_hdmi_check() -> hdmi_validate_vrr() is never
invoked.
Although later commit "drm/rockchip: Set mode_changed on vrr_enabled change"
sets mode_changed = true in rockchip_drm_atomic_check() when vrr_enabled
toggles, other HDMI drivers using drm_atomic_helper_connector_hdmi_check()
with drm_atomic_helper_check() will skip hdmi_validate_vrr() when VRR_ENABLED
toggles, leaving new_crtc_state->vrr_state uncomputed (or stale). Even on
Rockchip, updating VRR_{MIN,MAX}_{NUMERATOR,DENOMINATOR} while vrr_enabled
remains true does not set mode_changed or add the connector to the atomic
state, bypassing hdmi_validate_vrr() and VTEM infoframe regeneration.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=1
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 06/27] drm/crtc-helper: Add VRR helper functions
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:49 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:49 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] drm/crtc-helper: unsynchronized crtc_state mutation in VRR helpers
- [Medium] drm/crtc-helper: vrr->dynamic set and left true when VRR is disabled
--
commit 2e8aace704ca21ab8efb4aacd3b771407706ec16
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/crtc-helper: Add VRR helper functions
This commit adds drm_crtc_helper_vrr_flush() and drm_crtc_helper_vrr_vsync()
to update the internal VRR state in struct drm_crtc_state when a new frame is
ready or when a vertical blank ends, returning whether cur_vtotal changed.
> diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
> index 4a8cfe046e4ab..4ffb3b0b78780 100644
> --- a/drivers/gpu/drm/drm_crtc_helper.c
> +++ b/drivers/gpu/drm/drm_crtc_helper.c
> @@ -838,6 +838,69 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
> }
> EXPORT_SYMBOL(drm_crtc_helper_set_config);
>
> +/**
> + * drm_crtc_helper_vrr_flush - Update VRR state on frame ready
> + * @crtc_state: pointer to &struct drm_crtc_state of CRTC that has a new frame ready
> + *
> + * Update the VRR state by communicating that a frame is ready on
> + * @crtc_state.crtc.
> + *
> + * Should be called from the @crtc_state.crtc &drm_crtc_helper_funcs.atomic_flush
> + * hook.
> + *
> + * Returns:
> + * - %true if @crtc_state.vrr_state.cur_vtotal changed
> + * - %false otherwise
> + */
> +bool drm_crtc_helper_vrr_flush(struct drm_crtc_state *crtc_state)
> +{
> + struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
> +
> + if (!crtc_state->vrr_enabled) {
> + if (vrr->cur_vtotal) {
> + vrr->cur_vtotal = 0;
> + return true;
> + }
> + return false;
> + }
> +
> + if (vrr->cur_vtotal == vrr->base_vtotal)
> + return false;
> +
> + vrr->cur_vtotal = vrr->base_vtotal;
> +
> + return true;
> +}
> +EXPORT_SYMBOL(drm_crtc_helper_vrr_flush);
[Severity: High]
As reported by Sashiko on RFC [1], is it safe for drm_crtc_helper_vrr_vsync()
and drm_crtc_helper_vrr_flush() to mutate crtc_state->vrr_state in place
across commits and from the vblank interrupt handler?
Consider a sequence of two atomic commits when VRR is enabled:
1. Commit 1 runs atomic_flush and calls drm_crtc_helper_vrr_flush(state_1),
which sets state_1->vrr_state.cur_vtotal = base_vtotal and programs the
hardware vtotal to base_vtotal.
2. Commit 2 starts and calls drm_atomic_get_crtc_state() ->
__drm_atomic_helper_crtc_duplicate_state(), which copies crtc->state
(state_1) into state_2 via memcpy(), snapshotting
state_2->vrr_state.cur_vtotal = base_vtotal.
3. Before commit 2 calls drm_atomic_helper_swap_state() (or while waiting on
fences / hw_done), a vblank interrupt fires and the driver's vblank
handler calls drm_crtc_helper_vrr_vsync(crtc->state) on state_1, setting
state_1->vrr_state.cur_vtotal = max_vtotal and programming the hardware
vtotal to max_vtotal.
4. Commit 2 runs drm_atomic_helper_swap_state(), replacing crtc->state with
state_2, which still holds the stale vrr_state.cur_vtotal == base_vtotal
copied in step 2.
5. When commit 2 reaches atomic_flush and calls
drm_crtc_helper_vrr_flush(state_2), vrr->cur_vtotal == vrr->base_vtotal
evaluates to true and the helper returns false, causing the driver to skip
reprogramming the hardware vtotal back from max_vtotal to base_vtotal for
the new frame.
In addition, drm_crtc_helper_vrr_vsync() mutates crtc->state->vrr_state from
hardirq context without synchronization against
__drm_atomic_helper_crtc_duplicate_state(), drm_atomic_helper_swap_state(),
drm_crtc_helper_vrr_flush(), or state destruction after
drm_atomic_helper_commit_hw_done(). Should the mutable runtime VRR state live
on struct drm_crtc protected by an irq-safe spinlock rather than in
struct drm_crtc_state?
> +
> +/**
> + * drm_crtc_helper_vrr_vsync - Update VRR state on vblank end
> + * @crtc_state: pointer to &struct drm_crtc_state of CRTC the vblank occurred on
> + *
> + * Update the VRR state by communicating that a vertical blank has ended and a
> + * new frame is about to start.
> + *
> + * Returns:
> + * - %true if @crtc_state.vrr_state.cur_vtotal changed
> + * - %false otherwise
> + */
> +bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
> +{
> + struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
> + u16 old_vtotal = vrr->cur_vtotal;
> +
> + if (!crtc_state->vrr_enabled)
> + vrr->cur_vtotal = 0;
> + else
> + vrr->cur_vtotal = vrr->max_vtotal;
> +
> + if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
> + vrr->dynamic = true;
> +
> + return old_vtotal != vrr->cur_vtotal;
> +}
[Severity: Medium]
When crtc_state->vrr_enabled is false, drm_crtc_helper_vrr_vsync() sets
vrr->cur_vtotal = 0 and then checks:
if (abs_diff(vrr->cur_vtotal, old_vtotal) > 1)
vrr->dynamic = true;
If VRR was previously enabled so old_vtotal > 1 (including fixed-rate VRR
where vrr->dynamic was false), doesn't abs_diff(0, old_vtotal) > 1 evaluate
to true and set vrr->dynamic = true when VRR is disabled?
Neither drm_crtc_helper_vrr_vsync() nor drm_crtc_helper_vrr_flush() resets
vrr->dynamic to false in the !crtc_state->vrr_enabled path either, leaving
vrr->dynamic stuck at true after VRR is turned off. Should both helpers clear
vrr->dynamic = false when !crtc_state->vrr_enabled?
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-7-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=6
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 05/27] drm/connector: hdmi: Add VTEM EMP generation
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:53 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:53 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] drm/connector: hdmi: unpopulated adjusted_mode in hdmi_generate_emp_infoframe_vtem()
- [Medium] drm/connector: hdmi: invalid VTEM EMP state when vrr_display_mode is Off
--
commit 404c1e52433cdea0b822a43bb7471dc9ca224073
Author: Derek Foreman <derek.foreman@collabora.com>
drm/connector: hdmi: Add VTEM EMP generation
This commit adds VTEM Extended Metadata Packet (EMP) generation and state
tracking to the HDMI connector state helpers to support variable refresh
rate signalling on VRR-capable sinks.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> index c1a84e30441a6..497af307a690c 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> @@ -853,6 +853,61 @@ static int hdmi_generate_hdmi_vendor_infoframe(const struct drm_connector *conne
> return 0;
> }
>
> +static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connector,
> + struct drm_connector_state *conn_state)
> +{
> + const struct drm_display_info *info = &connector->display_info;
> + const struct drm_crtc_state *crtc_state =
> + drm_atomic_get_crtc_state(conn_state->state, conn_state->crtc);
> + struct drm_connector_hdmi_infoframe *infoframe =
> + &conn_state->hdmi.infoframes.vtem;
> + struct hdmi_emp_infoframe_vtem *vtem =
> + &infoframe->data.vtem;
> + const struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
> + int vfront;
> + u8 avi_vic;
> +
> + infoframe->set = false;
> +
> + if (!connector->hdmi.funcs->vtem.write_infoframe)
> + return 0;
> +
> + if (!info->hdmi.vrr_cap.supported)
> + return 0;
> +
> + if (conn_state->hdmi.infoframes.avi.set)
> + avi_vic = conn_state->hdmi.infoframes.avi.data.avi.video_code;
> + else
> + avi_vic = 0;
> +
> + hdmi_emp_infoframe_vtem_init(vtem);
> +
> + switch (conn_state->vrr_display_mode) {
> + case VRR_DISPLAY_MODE_GAME:
> + vtem->game_vrr_en = true;
> + vtem->m_const = !vrr->dynamic;
> + break;
> + case VRR_DISPLAY_MODE_OFF:
> + break;
[Severity: Medium]
When conn_state->vrr_display_mode is VRR_DISPLAY_MODE_OFF, breaking out of
the switch allows execution to reach infoframe->set = true at the end of
hdmi_generate_emp_infoframe_vtem().
Because new_frame->set is true, write_or_clear_infoframe() in
drm_atomic_helper_connector_hdmi_update_infoframes() will call
write_infoframe() instead of clear_infoframe():
drivers/gpu/drm/display/drm_hdmi_state_helper.c:write_or_clear_infoframe() {
if (new_frame->set)
return write_infoframe(connector, funcs, type, new_frame);
if (old_frame->set && !new_frame->set)
return clear_infoframe(connector, funcs, type);
...
}
In turn, hdmi_emp_infoframe_vtem_pack() packs a VTEM packet with
Data_Set_Length hardcoded to 4 (ptr[9] = 4) while game_vrr_en,
fva_factor_m1, base_vfront, and base_refresh_rate are all 0.
Should VRR_DISPLAY_MODE_OFF return 0 with infoframe->set left as false so
that write_or_clear_infoframe() invokes clear_infoframe() to stop VTEM EMP
transmission when VRR is disabled?
> + default:
> + return -EINVAL;
> + }
> +
> + if (vtem->game_vrr_en && !avi_vic) {
> + vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
> + vfront = crtc_state->adjusted_mode.crtc_vsync_start -
> + crtc_state->adjusted_mode.crtc_vdisplay;
[Severity: High]
Is crtc_state->adjusted_mode populated when
hdmi_generate_emp_infoframe_vtem() runs during atomic_check?
In drm_atomic_helper_check_modeset(), the connector atomic_check callbacks
(drm_atomic_helper_connector_hdmi_check() -> hdmi_generate_infoframes() ->
hdmi_generate_emp_infoframe_vtem()) run before mode_fixup() copies
new_crtc_state->mode into new_crtc_state->adjusted_mode:
drivers/gpu/drm/drm_atomic_helper.c:mode_fixup() {
for_each_new_crtc_in_state(state, crtc, new_crtc_state, i) {
if (!new_crtc_state->mode_changed &&
!new_crtc_state->connectors_changed)
continue;
drm_mode_copy(&new_crtc_state->adjusted_mode, &new_crtc_state->mode);
}
...
}
When enabling a previously disabled CRTC with a non-CEA mode (!avi_vic) and
VRR_DISPLAY_MODE_GAME, crtc_state->adjusted_mode is still zero-initialized,
so vfront is calculated as 0. When switching modes on an already-active
CRTC, crtc_state->adjusted_mode still holds the previous mode's timings
from atomic_duplicate_state(), causing vtem->base_vfront to be computed
from the old mode (or causing atomic_check to fail with -EINVAL if the old
mode had vfront > U8_MAX).
Should this compute vfront from crtc_state->mode (or
connector_state_get_mode(conn_state)), matching how vtem->base_refresh_rate
and hdmi_validate_vrr() read the mode timings?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=5
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 08/27] drm/connector: Add drm_display_info_is_vrr_capable
2026-11-05 15:51 ` Nicolas Frattaroli
(?)
@ 2026-11-05 16:58 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 16:58 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] drm/connector: false VRR capability in drm_display_info_is_vrr_capable()
--
commit 2c6b233292887595c2334a70a795f9f766983787
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/connector: Add drm_display_info_is_vrr_capable
This commit adds drm_display_info_is_vrr_capable() to check whether a sink
described by struct drm_display_info supports variable refresh rate signals.
It checks info->hdmi.vrr_cap.supported for HDMI sinks and
info->monitor_range.min_vfreq > 0 for other displays.
> diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c
> index 65c14b6e47a6f..a44afba8c5ff2 100644
> --- a/drivers/gpu/drm/drm_connector.c
> +++ b/drivers/gpu/drm/drm_connector.c
> @@ -1378,6 +1378,25 @@ int drm_display_info_set_bus_formats(struct drm_display_info *info,
> }
> EXPORT_SYMBOL(drm_display_info_set_bus_formats);
>
> +/**
> + * drm_display_info_is_vrr_capable - Check whether sink is VRR capable
> + * @info: pointer to a &struct drm_display_info representing the sink
> + *
> + * Check whether the sink supports variable refresh rate signals.
> + *
> + * Returns:
> + * - true if variable refresh rate is supported by the sink
> + * - false otherwise
> + */
> +bool drm_display_info_is_vrr_capable(const struct drm_display_info *info)
> +{
> + if (info->is_hdmi)
> + return info->hdmi.vrr_cap.supported;
> +
> + return info->monitor_range.min_vfreq > 0;
> +}
[Severity: Medium]
As reported by Sashiko on RFC [1], in drm_display_info_is_vrr_capable() in
drivers/gpu/drm/drm_connector.c, can returning info->monitor_range.min_vfreq > 0
for any sink where info->is_hdmi is false falsely report VRR support on
non-VRR displays?
In drivers/gpu/drm/drm_edid.c, drm_get_monitor_range() -> get_monitor_range()
populates info->monitor_range from any standard EDID 1.4+ continuous-frequency
(DRM_EDID_FEATURE_CONTINUOUS_FREQ) Display Range Limits descriptor (tag 0xFD)
with DRM_EDID_RANGE_LIMITS_ONLY_FLAG:
drivers/gpu/drm/drm_edid.c:get_monitor_range() {
...
monitor_range->min_vfreq = range->min_vfreq;
monitor_range->max_vfreq = range->max_vfreq;
...
}
Existing VRR checks in drivers/gpu/drm/i915/display/intel_vrr.c and
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c do not rely on
min_vfreq > 0 alone. Both check for DPCD MSA timing ignore support and require
a valid range span larger than 10 Hz:
drivers/gpu/drm/i915/display/intel_vrr.c:intel_vrr_is_capable() {
...
if (!info->monitor_range.min_vfreq || !info->monitor_range.max_vfreq ||
info->monitor_range.min_vfreq > info->monitor_range.max_vfreq)
return false;
return info->monitor_range.max_vfreq - info->monitor_range.min_vfreq > 10;
}
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c:amdgpu_dm_update_freesync_caps() {
...
if (amdgpu_dm_connector->dc_link &&
amdgpu_dm_connector->dc_link->dpcd_caps.allow_invalid_MSA_timing_param &&
amdgpu_dm_connector->max_vfreq - amdgpu_dm_connector->min_vfreq > 10)
freesync_capable = true;
...
}
Without checking DPCD capabilities (which are not stored in struct
drm_display_info) or validating max_vfreq and max_vfreq - min_vfreq > 10, a
standard non-VRR monitor with a 50-60 Hz range descriptor, a malformed range
descriptor, or a DVI/non-VSDB sink attached to an HDMI connector (where
info->is_hdmi is false) will cause drm_display_info_is_vrr_capable() to return
true. On HDMI encoders using this helper, that advertises vrr_capable as true
even though HDMI VRR validation later rejects VRR commits with -EOPNOTSUPP.
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-9-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=8
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 09/27] drm: Add VRR target frame rate properties
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:07 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:07 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] drm: missing DRM_MODE_PROP_ATOMIC flag on VRR limiter CRTC properties
- [Medium] drm: missing min <= max VRR rate check in drm_atomic_crtc_check()
--
commit 221e142e46603679a11c0d1b71599449f1d7a39c
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm: Add VRR target frame rate properties
This commit introduces four CRTC properties (VRR_MIN_NUMERATOR,
VRR_MIN_DENOMINATOR, VRR_MAX_NUMERATOR, and VRR_MAX_DENOMINATOR) to allow
userspace to configure a target frame rate range for Variable Refresh Rate
as numerator/denominator fractions.
> diff --git a/drivers/gpu/drm/drm_atomic.c b/drivers/gpu/drm/drm_atomic.c
> index 9bb761eda59f4..da5c6925c349f 100644
> --- a/drivers/gpu/drm/drm_atomic.c
> +++ b/drivers/gpu/drm/drm_atomic.c
> @@ -489,6 +489,7 @@ EXPORT_SYMBOL(drm_atomic_get_crtc_state);
> static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
> const struct drm_crtc_state *new_crtc_state)
> {
> + const struct drm_crtc_vrr_state *vrr = &new_crtc_state->vrr_state;
> struct drm_crtc *crtc = new_crtc_state->crtc;
>
> /* NOTE: we explicitly don't enforce constraints such as primary
> @@ -544,6 +545,19 @@ static int drm_atomic_crtc_check(const struct drm_crtc_state *old_crtc_state,
> return -EINVAL;
> }
>
> + /*
> + * Reject VRR limiter ranges that only specify one end of the limit, or
> + * specify a numerator > 0 with a denominator that is 0.
> + */
> + if ((!vrr->vrr_min_n || !vrr->vrr_min_d ||
> + !vrr->vrr_max_n || !vrr->vrr_max_d) &&
> + (vrr->vrr_min_n > 0 || vrr->vrr_max_n > 0)) {
> + drm_dbg_atomic(crtc->dev,
> + "[CRTC:%d:%s] incomplete VRR limiter range\n",
> + crtc->base.id, crtc->name);
> + return -EINVAL;
> + }
[Severity: Medium]
As reported by Sashiko on RFC [1], should drm_atomic_crtc_check() also reject
inverted VRR target frame rate ranges where the minimum rate fraction exceeds
the maximum rate fraction?
In include/drm/drm_mode_config.h, the kerneldoc for struct drm_mode_config
documents that prop_vrr_max_numerator divided by prop_vrr_max_denominator must
result in a frame rate equal to or above prop_vrr_min_numerator divided by
prop_vrr_min_denominator.
However, drm_atomic_crtc_check() only checks that all four values are non-zero
when either numerator is positive. If userspace submits an atomic commit via
DRM_IOCTL_MODE_ATOMIC with non-zero limiter values where vrr_min_n / vrr_min_d
is greater than vrr_max_n / vrr_max_d (for example while vrr_enabled is false,
or on a non-HDMI connector), the check succeeds and commits the inverted range
into drm_crtc_state.
Would it make sense to check:
mul_u32_u32(vrr->vrr_min_n, vrr->vrr_max_d) >
mul_u32_u32(vrr->vrr_max_n, vrr->vrr_min_d)
when the limiter range is non-zero in drm_atomic_crtc_check()?
[ ... ]
> diff --git a/drivers/gpu/drm/drm_mode_config.c b/drivers/gpu/drm/drm_mode_config.c
> index 48a56f23dadbb..da967d60f832a 100644
> --- a/drivers/gpu/drm/drm_mode_config.c
> +++ b/drivers/gpu/drm/drm_mode_config.c
> @@ -535,6 +535,26 @@ static int drm_mode_create_standard_properties(struct drm_device *dev)
> return -ENOMEM;
> dev->mode_config.prop_vrr_enabled = prop;
>
> + prop = drm_property_create_range(dev, 0, "VRR_MIN_NUMERATOR", 0, U32_MAX);
> + if (!prop)
> + return -ENOMEM;
> + dev->mode_config.prop_vrr_min_numerator = prop;
> +
> + prop = drm_property_create_range(dev, 0, "VRR_MIN_DENOMINATOR", 0, U32_MAX);
> + if (!prop)
> + return -ENOMEM;
> + dev->mode_config.prop_vrr_min_denominator = prop;
> +
> + prop = drm_property_create_range(dev, 0, "VRR_MAX_NUMERATOR", 0, U32_MAX);
> + if (!prop)
> + return -ENOMEM;
> + dev->mode_config.prop_vrr_max_numerator = prop;
> +
> + prop = drm_property_create_range(dev, 0, "VRR_MAX_DENOMINATOR", 0, U32_MAX);
> + if (!prop)
> + return -ENOMEM;
> + dev->mode_config.prop_vrr_max_denominator = prop;
[Severity: Medium]
As reported by Sashiko on RFC [2], should these four properties be created
with the DRM_MODE_PROP_ATOMIC flag in drm_mode_create_standard_properties()?
Because flags is passed as 0 to drm_property_create_range(),
drm_mode_object_get_properties() exposes these properties to legacy non-atomic
userspace clients. Non-atomic clients update properties one at a time via
DRM_IOCTL_MODE_OBJ_SETPROPERTY:
drm_mode_obj_set_property_ioctl()
-> set_property_atomic()
-> drm_atomic_commit()
-> drm_atomic_check_only()
-> drm_atomic_crtc_check()
Since drm_atomic_crtc_check() rejects any state where only part of the
limiter range is non-zero with -EINVAL, a legacy client cannot transition
from (0, 0, 0, 0) to an active range or clear an active range back to
(0, 0, 0, 0) one property at a time without failing on the intermediate
commits.
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-13-2fcd7d011646@collabora.com
[2] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-13-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=9
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 10/27] drm/connector: Add vrr_display_{min,max} connector properties
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:08 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:08 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] drm/hdmi: handle vrr_max == 0 in drm_connector_hdmi_update_vrr_limits()
- [Medium] drm/hdmi: stale VRR limits on unplug in drm_connector_hdmi_update_vrr_limits()
- [Medium] drm/hdmi: unclamped cnmvrr max_hz in drm_connector_hdmi_update_vrr_limits()
--
commit 8141ba1132150753b66c1d4aef2157bc2d78d4cb
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/connector: Add vrr_display_{min,max} connector properties
This commit adds the immutable vrr_display_min and vrr_display_max connector
properties to expose the minimum and maximum integer refresh rates supported
with VRR in the current mode. It also introduces helpers to attach these
properties and update their values for HDMI connectors.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_helper.c b/drivers/gpu/drm/display/drm_hdmi_helper.c
> index f85cce3d62547..8f325d5e496d1 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_helper.c
> @@ -730,3 +730,58 @@ int drm_connector_hdmi_sync_scdc(struct drm_connector *connector, bool plugged,
> return ret;
> }
> EXPORT_SYMBOL(drm_connector_hdmi_sync_scdc);
> +
> +/**
> + * drm_connector_hdmi_update_vrr_limits - update VRR display limit properties
> + * @conn: pointer to the &struct drm_connector to set the properties on
> + * @mode: pointer to the &struct drm_display_mode to consider while updating
> + *
> + * Update the "vrr_display_min" and "vrr_display_max" properties on @conn based
> + * on its HDMI sink info and the current mode. Does not consider any display
> + * protocol other than HDMI.
> + *
> + * Will set both min and max to 0 if max happens to end up below min, which can
> + * happen with CNMVRR==0 and a lower refresh rate @mode.
> + *
> + * Return: %0 on success, negative errno otherwise
> + */
> +int drm_connector_hdmi_update_vrr_limits(struct drm_connector *conn,
> + const struct drm_display_mode *mode)
> +{
> + struct drm_mode_object *obj = &conn->base;
> + const struct drm_display_info *info;
> + u16 min_hz, max_hz;
> +
> + if (!conn->vrr_display_min_property || !conn->vrr_display_max_property)
> + return -ENOENT;
> +
> + info = &conn->display_info;
> +
> + if (!info->is_hdmi)
> + return -EINVAL;
> +
> + if (conn->status != connector_status_connected ||
> + !info->hdmi.vrr_cap.supported) {
> + drm_object_property_set_value(obj, conn->vrr_display_min_property, 0);
> + drm_object_property_set_value(obj, conn->vrr_display_max_property, 0);
> +
> + return 0;
> + }
[Severity: Medium]
When an HDMI VRR display is unplugged or replaced with a non-HDMI sink (such
as DVI), drm_edid_connector_update() calls drm_reset_display_info():
drivers/gpu/drm/drm_edid.c:drm_reset_display_info() {
...
info->is_hdmi = false;
info->has_audio = false;
info->has_hdmi_infoframe = false;
info->rgb_quant_range_selectable = false;
memset(&info->hdmi, 0, sizeof(info->hdmi));
...
}
Because drm_connector_hdmi_update_vrr_limits() checks !info->is_hdmi and
returns -EINVAL before checking conn->status != connector_status_connected,
won't calling this helper after an unplug or sink swap fail to reset
vrr_display_min_property and vrr_display_max_property to 0, leaving stale
non-zero limits exposed to userspace?
> +
> + min_hz = info->hdmi.vrr_cap.vrr_min;
> +
> + if (info->hdmi.vrr_cap.cnmvrr)
> + max_hz = info->hdmi.vrr_cap.vrr_max;
[Severity: Medium]
Should max_hz still be clamped to drm_mode_vrefresh(mode) when
info->hdmi.vrr_cap.cnmvrr is set?
In HDMI 2.1, negative M_VRR (cnmvrr) only allows shortening Vfront by at most
ceil(Vfront_base / 4) lines in constant-rate CinemaVRR mode for fractional
pull-up (such as 59.94 Hz to 60 Hz), and cannot increase a lower-rate mode
(such as 60 Hz or 24 Hz) up to vrr_max (such as 120 Hz) at that mode's pixel
clock.
In addition, hdmi_validate_vrr() in
drivers/gpu/drm/display/drm_hdmi_state_helper.c unconditionally caps the
maximum refresh rate to mode_refresh regardless of cnmvrr:
if (!max_vfreq_n || max_vfreq_n > mode_refresh)
max_vfreq_n = mode_refresh;
...
vrr_state->base_vtotal = max(vrr_state->base_vtotal, mode->crtc_vtotal);
Leaving max_hz unclamped when cnmvrr is true advertises a vrr_display_max
above mode_refresh that hdmi_validate_vrr() will reject, and also causes
modes with drm_mode_vrefresh(mode) <= min_hz (for example, a 24 Hz mode on a
48-120 Hz display) to bypass the max_hz <= min_hz check below instead of
reporting 0.
> + else
> + max_hz = min(drm_mode_vrefresh(mode), info->hdmi.vrr_cap.vrr_max);
> +
> + /* VRR is impossible in the current mode */
> + if (max_hz <= min_hz)
> + min_hz = max_hz = 0;
[Severity: Medium]
What happens here when a VRR-capable HDMI 2.1 sink leaves vrr_max as 0 in its
EDID HF-VSDB to indicate that the upper VRR bound is the video mode's base
refresh rate?
In drivers/gpu/drm/drm_edid.c, drm_parse_hdmi_gaming_info() treats
vrr_max == 0 as valid when VRR is supported:
vrr->vrr_min = db[9] & DRM_EDID_VRR_MIN_MASK;
vrr->supported = (vrr->vrr_min > 0 && vrr->vrr_min <= 48);
if (cea_db_payload_len(db) < 10)
return;
vrr->vrr_max = (db[9] & DRM_EDID_VRR_MAX_UPPER_MASK) << 2 | db[10];
vrr->supported &= (vrr->vrr_max == 0 || vrr->vrr_max >= 100);
And hdmi_validate_vrr() in drivers/gpu/drm/display/drm_hdmi_state_helper.c
handles a 0 maximum by falling back to mode_refresh:
if (!max_vfreq_n || max_vfreq_n > mode_refresh)
max_vfreq_n = mode_refresh;
In drm_connector_hdmi_update_vrr_limits(), however, if
info->hdmi.vrr_cap.vrr_max is 0, both branches of the if/else evaluate
max_hz to 0, which then triggers max_hz <= min_hz and sets both
vrr_display_min_property and vrr_display_max_property to 0. Should a vrr_max
of 0 fall back to drm_mode_vrefresh(mode) instead?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=10
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 11/27] drm: Implement VRR rate limiting
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:17 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:17 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] drm: NULL pointer dereference on old_conn_state->crtc in hdmi_validate_vrr()
- [High] drm: zero-denominator WARN_ON() in cmp_fraction() from hdmi_validate_vrr()
- [Medium] drm: signed 32-bit overflow in cur_err in drm_crtc_helper_vrr_vsync()
- [Medium] drm: fixed-rate VRR validation bugs in hdmi_validate_vrr_limits()
- [Medium] drm: lost vrr_state updates due to in-place mutation in drm_crtc_helper_vrr_vsync()
- [Low] drm: redundant overflow check and unused limit_d in hdmi_validate_vrr_limits()
--
commit 41d8f0b98056d34444a098f24f445d7ca3d40b1e
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm: Implement VRR rate limiting
This commit implements VRR target frame rate limiting in the HDMI state
and CRTC VRR helpers, allowing userspace to restrict the VRR refresh rate
range or configure a fixed target frame rate with per-frame error dithering.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> index 497af307a690c..1e576dfbd856d 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> @@ -1051,15 +1052,60 @@ static int hdmi_calculate_vtotal(const struct drm_display_mode *mode,
> return 0;
> }
>
> +static int hdmi_validate_vrr_limits(struct drm_crtc_state *crtc_state, u16 vrr_min)
> +{
> + struct drm_crtc_vrr_state *vrr_state = &crtc_state->vrr_state;
> + const struct drm_display_mode *mode = &crtc_state->mode;
> + u32 limit_d;
> + u8 round;
> + int ret;
> +
> + /* No limiter, don't need to validate, but reset the error */
> + if (!drm_crtc_helper_vrr_is_fixed_rate(vrr_state)) {
> + vrr_state->per_frame_err = 0;
> + vrr_state->cur_err = 0;
> + return 0;
> + }
> +
> + /* Don't allow rates equal to or below the minimum VRR rate / 1.001 */
> + if (cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
> + vrr_min * 1000, 1001) < 0)
> + return -EINVAL;
> +
> + if (check_mul_overflow(mode->crtc_htotal, vrr_state->vrr_min_n, &limit_d))
> + return -ERANGE;
[Severity: Low]
As reported by Sashiko on RFC [1], this isn't a bug, but is this
check_mul_overflow() into the unused limit_d variable redundant since
hdmi_calculate_vtotal() immediately checks mode->crtc_htotal * rate_n as
its first operation?
> +
> + ret = hdmi_calculate_vtotal(&crtc_state->mode, vrr_state->vrr_min_n,
> + vrr_state->vrr_min_d, &vrr_state->base_vtotal,
> + &vrr_state->denom, &vrr_state->per_frame_err);
> + if (ret)
> + return ret;
> +
> + /* Allow rounding up for the vtotal check below */
> + if (vrr_state->per_frame_err >= vrr_state->denom / 2)
> + round = 1;
> + else
> + round = 0;
> +
> + if (vrr_state->base_vtotal + round < mode->crtc_vtotal)
> + return -EINVAL;
[Severity: Medium]
As reported by Sashiko on RFC [1], can hdmi_validate_vrr_limits() and
hdmi_validate_vrr() allow invalid hardware timings in fixed-rate mode?
First, when per_frame_err >= denom / 2, round is 1, so the check above
accepts base_vtotal == mode->crtc_vtotal - 1 without clamping base_vtotal
to mode->crtc_vtotal. When drm_crtc_helper_vrr_vsync() runs on frames
where cur_err < denom / 2 (including the initial frame where cur_err is 0),
it sets cur_vtotal = base_vtotal, programming a vtotal one line smaller
than mode->crtc_vtotal.
Second, if hdmi_calculate_vtotal() produces base_vtotal == U16_MAX with
per_frame_err >= denom / 2, drm_crtc_helper_vrr_vsync() will compute
vrr->cur_vtotal = vrr->base_vtotal + 1, wrapping the u16 cur_vtotal to 0.
Third, in hdmi_validate_vrr(), the check against max_vfreq_n is placed
inside the if (!is_fixed) block, so a fixed target rate exceeding
info->hdmi.vrr_cap.vrr_max (when vrr_max < mode_refresh) is not rejected.
> +
> + return 0;
> +}
> +
> static int hdmi_validate_vrr(struct drm_connector *connector,
> struct drm_atomic_commit *state)
> {
> struct drm_connector_state *new_conn_state =
> drm_atomic_get_new_connector_state(state, connector);
> + struct drm_connector_state *old_conn_state =
> + drm_atomic_get_old_connector_state(state, connector);
> struct drm_crtc_state *new_crtc_state =
> drm_atomic_get_new_crtc_state(state, new_conn_state->crtc);
> struct drm_crtc_vrr_state *vrr_state =
> &new_crtc_state->vrr_state;
> + struct drm_crtc_vrr_state *old_vrr_state;
> + struct drm_crtc_state *old_crtc_state = NULL;
> struct drm_display_info *info = &connector->display_info;
> const struct drm_display_mode *mode = &new_crtc_state->mode;
> struct drm_device *dev = connector->dev;
> @@ -1068,10 +1114,14 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
> u32 min_vfreq_d = 1;
> u32 max_vfreq_n = info->hdmi.vrr_cap.vrr_max;
> u32 max_vfreq_d = 1;
> + bool is_fixed;
>
> if (!new_crtc_state->vrr_enabled)
> return 0;
>
> + if (old_conn_state)
> + old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
[Severity: High]
Can this dereference a NULL pointer when enabling VRR on a connector that
was previously disabled or unassigned?
When a connector in the atomic commit is newly routed to a CRTC with VRR
enabled, old_conn_state is non-NULL, but old_conn_state->crtc is NULL.
Passing old_conn_state->crtc to drm_atomic_get_old_crtc_state() invokes
drm_crtc_index(NULL), which immediately dereferences crtc->index:
include/drm/drm_atomic.h:drm_atomic_get_old_crtc_state() {
return state->crtcs[drm_crtc_index(crtc)].old_state;
}
Also, if a connector is moved directly from one CRTC to another,
old_conn_state->crtc points to the previous CRTC rather than
new_conn_state->crtc from which new_crtc_state->vrr_state was duplicated.
Should this look up the old CRTC state via new_conn_state->crtc instead?
> +
> if (!info->hdmi.vrr_cap.supported)
> return -EOPNOTSUPP;
>
> @@ -1087,6 +1137,21 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
> if (!max_vfreq_n || max_vfreq_n > mode_refresh)
> max_vfreq_n = mode_refresh;
>
> + is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
> + if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
> + min_vfreq_n = 24;
> +
> + /* If the VRR target changed, make sure to reset the error accumulator */
> + if (old_crtc_state) {
> + old_vrr_state = &old_crtc_state->vrr_state;
> + if (drm_crtc_helper_vrr_is_fixed_rate(old_vrr_state) != is_fixed ||
> + cmp_fraction(vrr_state->vrr_min_n, vrr_state->vrr_min_d,
> + old_vrr_state->vrr_min_n, old_vrr_state->vrr_min_d) ||
> + cmp_fraction(vrr_state->vrr_max_n, vrr_state->vrr_max_d,
> + old_vrr_state->vrr_max_n, old_vrr_state->vrr_max_d))
> + vrr_state->cur_err = 0;
> + }
[Severity: High]
Will this trigger the WARN_ON(!b || !d) in cmp_fraction() when VRR is
enabled without a fixed target rate?
When neither old_vrr_state nor vrr_state is a fixed rate (for example,
normal unconstrained VRR where vrr_min_d and vrr_max_d are 0 in both
states, or when transitioning between unconstrained VRR and a bounded
range), drm_crtc_helper_vrr_is_fixed_rate(old_vrr_state) != is_fixed is
false. Evaluation then falls through to cmp_fraction() with zero
denominators:
drivers/gpu/drm/display/drm_hdmi_state_helper.c:cmp_fraction() {
u64 a_d = mul_u32_u32(a, d);
u64 b_c = mul_u32_u32(b, c);
if (WARN_ON(!b || !d))
return -EDOM;
...
}
> diff --git a/drivers/gpu/drm/drm_crtc_helper.c b/drivers/gpu/drm/drm_crtc_helper.c
> index 4ffb3b0b78780..dd696f0c50169 100644
> --- a/drivers/gpu/drm/drm_crtc_helper.c
> +++ b/drivers/gpu/drm/drm_crtc_helper.c
[ ... ]
> @@ -889,10 +908,21 @@ bool drm_crtc_helper_vrr_vsync(struct drm_crtc_state *crtc_state)
> struct drm_crtc_vrr_state *vrr = &crtc_state->vrr_state;
> u16 old_vtotal = vrr->cur_vtotal;
>
> - if (!crtc_state->vrr_enabled)
> + if (!crtc_state->vrr_enabled) {
> vrr->cur_vtotal = 0;
> - else
> + vrr->cur_err = 0;
> + } else if (drm_crtc_helper_vrr_is_fixed_rate(vrr)) {
> + if (vrr->cur_err >= (int) (vrr->denom / 2)) {
> + vrr->cur_err -= vrr->denom;
> + vrr->cur_vtotal = vrr->base_vtotal + 1;
> + } else {
> + vrr->cur_vtotal = vrr->base_vtotal;
> + }
> +
> + vrr->cur_err += vrr->per_frame_err;
[Severity: Medium]
Can vrr->cur_err overflow a signed 32-bit int here?
In struct drm_crtc_vrr_state, denom and per_frame_err are u32, while
cur_err is a signed 32-bit int. hdmi_calculate_vtotal() allows denom
(mode->crtc_htotal * rate_n) to be any u32 up to U32_MAX, and
per_frame_err can be up to denom - 1.
When vrr->cur_err < (int)(vrr->denom / 2), vrr->denom is not subtracted
before adding vrr->per_frame_err, so the sum can reach roughly
1.5 * denom - 2. Whenever denom exceeds 1,431,655,765 (for example,
crtc_htotal = 2200 and vrr_min_n = 1,000,000), vrr->cur_err +=
vrr->per_frame_err overflows INT_MAX and wraps negative.
[Severity: Medium]
Also, can in-place updates to vrr->cur_err, vrr->cur_vtotal, and
vrr->dynamic in drm_crtc_helper_vrr_vsync() be lost across atomic commits?
During atomic commits, drm_atomic_get_crtc_state() duplicates crtc->state
into new_crtc_state under crtc->mutex while the vblank interrupt handler
can concurrently call drm_crtc_helper_vrr_vsync(crtc->state) and mutate
the old crtc->state->vrr_state in place. When drm_atomic_helper_swap_state()
later installs new_crtc_state into crtc->state, any vrr_state updates
performed by drm_crtc_helper_vrr_vsync() after atomic_duplicate_state()
are overwritten by the stale copy in new_crtc_state.
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-14-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=11
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 12/27] drm/rockchip: dw_hdmi_qp: Add VRR support
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:30 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:30 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] drm/rockchip: dw_hdmi_qp: live VRR property mutation in encoder atomic_check()
--
commit cf4752529f9d39b6df9beff8735c1aec029921cc
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/rockchip: dw_hdmi_qp: Add VRR support
This commit attaches VRR connector properties on RK3588 in
dw_hdmi_qp_rockchip_bind() and updates vrr_capable and the VRR display limit
properties in dw_hdmi_qp_rockchip_encoder_atomic_check().
> diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
> index f2a1ea8dfefa5..dcde50bca36bf 100644
> --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
> +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
[ ... ]
> @@ -195,6 +198,7 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
> struct drm_crtc_state *crtc_state,
> struct drm_connector_state *conn_state)
> {
> + struct drm_connector *connector = conn_state->connector;
> struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state);
> struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder);
> union phy_configure_opts phy_cfg = {};
> @@ -205,6 +209,12 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder,
> if (ingest_fmt < 0)
> return -EINVAL;
>
> + if (connector->vrr_capable_property) {
> + drm_connector_set_vrr_capable_property(connector,
> + drm_display_info_is_vrr_capable(&connector->display_info));
> + drm_connector_hdmi_update_vrr_limits(connector, &crtc_state->mode);
> + }
[Severity: Medium]
As reported by Sashiko on RFC [1], is it safe to update the live connector
VRR properties inside dw_hdmi_qp_rockchip_encoder_atomic_check()?
Both drm_connector_set_vrr_capable_property() and
drm_connector_hdmi_update_vrr_limits() call drm_object_property_set_value()
on connector->base, which directly mutates connector->base.properties->values[]
because vrr_capable, vrr_display_min, and vrr_display_max are immutable
properties rather than transactional drm_connector_state fields.
When userspace performs a DRM_MODE_ATOMIC_TEST_ONLY commit to probe candidate
modes, or when an atomic check fails later in the pipeline, this path:
drm_mode_atomic_ioctl()
drm_atomic_check_only()
drm_atomic_helper_check()
drm_atomic_helper_check_modeset()
mode_fixup()
dw_hdmi_qp_rockchip_encoder_atomic_check()
immediately overwrites the live user-visible vrr_display_min and
vrr_display_max values with limits derived from the uncommitted
crtc_state->mode.
In addition, mode_fixup() in drivers/gpu/drm/drm_atomic_helper.c skips the
encoder atomic_check when a connector has no CRTC or encoder assigned:
if (!new_conn_state->crtc || !new_conn_state->best_encoder)
continue;
When a VRR-capable display is connected at boot or hotplugged while the
connector is disabled, DRM_IOCTL_MODE_GETCONNECTOR probes the connector and
updates EDID without running dw_hdmi_qp_rockchip_encoder_atomic_check(),
leaving vrr_capable at 0 until a modeset occurs. Conversely, when a VRR
display is unplugged and its CRTC is disabled (new_conn_state->crtc == NULL),
mode_fixup() skips the encoder check and leaves vrr_capable stuck at 1 and
vrr_display_min / vrr_display_max holding the disconnected sink's values.
Finally, connector->display_info and connector->status (read by
drm_display_info_is_vrr_capable() and drm_connector_hdmi_update_vrr_limits())
are protected by dev->mode_config.mutex, whereas atomic_check only holds
connection_mutex and crtc->mutex, which can race with concurrent hotplug
updates in drm_bridge_connector_handle_hpd().
Should vrr_capable be updated in the connector hotplug / EDID probe path
while holding mode_config.mutex, and should the mode-dependent VRR limits be
updated during atomic commit rather than atomic_check?
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-10-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=12
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 14/27] drm/rockchip: vop2: Enable VRR
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:40 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:40 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] drm/rockchip: vop2: vop2_vp_handle_vblank() skips VRR vsync without vp->event
- [High] drm/rockchip: vop2: vrr_enabled guards prevent VRR state reset and modeset sync
- [High] drm/rockchip: vop2: missing VRR state validation in vop2_crtc_atomic_check()
- [Medium] drm/rockchip: vop2: rk3576_vp_isr() omits vop2_vp_handle_vblank() refactoring
- [Medium] drm/rockchip: vop2: race on vrr_state across crtc->state duplication and swap
- [Medium] drm/rockchip: vop2: vop2_crtc_atomic_flush() clears VRR vblank IRQ
--
commit fd4256e3d8d21803c823cd3f851049669be3d956
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/rockchip: vop2: Enable VRR
This commit enables Variable Refresh Rate (VRR) front porch adjustment on
Rockchip VOP2 using DRM CRTC VRR state tracking helpers, refactored vblank
event handling, and VRR limiter properties on RK3588 video ports.
> diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
> index bfb3841425687..d93b490257952 100644
> --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
> +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
[ ... ]
> @@ -2011,6 +2012,25 @@ static void vop2_crtc_atomic_begin(struct drm_crtc *crtc,
> vop2->ops->setup_overlay(vp);
> }
>
> +static void vop2_vp_set_vrr_vtotal(struct vop2_video_port *vp, unsigned int vtotal)
> +{
> + const u32 mask = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL |
> + RK3568_VP_DSP_VTOTAL_VS_END__IMD;
> +
> + if (!vtotal) {
> + vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END,
> + RK3568_VP_DSP_VTOTAL_VS_END__IMD, 0);
> + return;
> + }
> +
> + /* Value is 13 bits at most, saturate it if too large */
> + if (vtotal > RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16)
> + vtotal = RK3568_VP_DSP_VTOTAL_VS_END__DSP_VTOTAL >> 16;
> +
> + vop2_vp_update_bits(vp, RK3568_VP_DSP_VTOTAL_VS_END, mask,
> + vtotal << 16 | RK3568_VP_DSP_VTOTAL_VS_END__IMD);
> +}
> +
> static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
> struct drm_atomic_commit *state)
> {
> @@ -2029,6 +2049,11 @@ static void vop2_crtc_atomic_flush(struct drm_crtc *crtc,
>
> spin_lock_irq(&crtc->dev->event_lock);
>
> + if (crtc_state->vrr_enabled) {
> + if (drm_crtc_helper_vrr_flush(crtc_state))
> + vop2_vp_set_vrr_vtotal(vp, crtc_state->vrr_state.cur_vtotal);
> + }
[Severity: High]
As reported by Sashiko on RFC [1], does guarding drm_crtc_helper_vrr_flush()
and drm_crtc_helper_vrr_vsync() with vrr_enabled prevent the VRR state from
being reset when VRR is disabled?
Both drm_crtc_helper_vrr_flush() and drm_crtc_helper_vrr_vsync() have
!crtc_state->vrr_enabled branches that reset vrr->cur_vtotal = 0 (and
vrr->cur_err = 0 in vsync) and return true so vop2_vp_set_vrr_vtotal() can
take its !vtotal branch and clear RK3568_VP_DSP_VTOTAL_VS_END__IMD.
Because both vop2_crtc_atomic_flush() and vop2_vp_handle_vblank() check
vrr_enabled before calling those helpers, the reset paths and the !vtotal
branch in vop2_vp_set_vrr_vtotal() are never reached.
Furthermore, vop2_crtc_duplicate_state() copies rockchip_crtc_state via
kmemdup(), preserving any stale vrr_state.cur_vtotal across a modeset or
VRR disable/re-enable cycle. When vop2_crtc_atomic_enable() writes
mode->crtc_vtotal << 16 | vsync_len (with IMD = 0) to
RK3568_VP_DSP_VTOTAL_VS_END, a stale cur_vtotal that already equals
base_vtotal causes drm_crtc_helper_vrr_flush() (and
drm_crtc_helper_vrr_vsync() in integer fixed-rate mode) to see no change and
return false, leaving hardware running at mode->crtc_vtotal with IMD cleared.
> +
> if (crtc->state->event) {
> WARN_ON(drm_crtc_vblank_get(crtc));
> vp->event = crtc->state->event;
[Severity: Medium]
Can calling vop2_vp_set_vrr_vtotal() before drm_crtc_vblank_get() in
vop2_crtc_atomic_flush() lose the vblank interrupt for a VRR page flip?
If the vblank disable timer has expired during an idle period while the video
port is scanning out in the extended VRR front porch (at max_vtotal),
VP_INT_FS_FIELD is masked in RK3568_VP_INT_EN(vp->id).
When vop2_crtc_atomic_flush() then runs:
vop2_crtc_atomic_flush()
vop2_vp_set_vrr_vtotal()
/* writes base_vtotal with RK3568_VP_DSP_VTOTAL_VS_END__IMD */
drm_crtc_vblank_get()
vop2_crtc_enable_vblank()
vop2_crtc_enable_irq(vp, VP_INT_FS_FIELD)
vop2_writel(vop2, RK3568_VP_INT_CLR(vp->id), irq << 16 | irq);
vop2_writel(vop2, RK3568_VP_INT_EN(vp->id), irq << 16 | irq);
Reducing DSP_VTOTAL to base_vtotal with the immediate bit set can end the
current frame immediately in hardware and assert VP_INT_FS_FIELD before
drm_crtc_vblank_get() is called. vop2_crtc_enable_irq() then clears
RK3568_VP_INT_CLR(vp->id) before unmasking the interrupt, dropping the
pending vblank IRQ and delaying vp->event and drm_crtc_helper_vrr_vsync()
by a full frame.
> @@ -2371,6 +2396,30 @@ static irqreturn_t rk3576_vp_isr(int irq, void *data)
> return ret;
> }
[Severity: Medium]
As reported by Sashiko on RFC [2], should rk3576_vp_isr() also call
vop2_vp_handle_vblank(vp) instead of keeping the open-coded vblank event
handling block?
In drivers/gpu/drm/rockchip/rockchip_drm_vop2.c:rk3576_vp_isr():
if (irqs & VP_INT_FS_FIELD) {
drm_crtc_handle_vblank(crtc);
spin_lock(&crtc->dev->event_lock);
if (vp->event) {
u32 val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
if (!(val & BIT(vp->id))) {
drm_crtc_send_vblank_event(crtc, vp->event);
vp->event = NULL;
drm_crtc_vblank_put(crtc);
}
}
spin_unlock(&crtc->dev->event_lock);
ret = IRQ_HANDLED;
}
vop2_isr() only handles video port interrupts for vop2->version <
VOP_VERSION_RK3576, while rk3576_vp_isr() still duplicates the old event
handling without calling vop2_vp_handle_vblank(vp).
> +
> +static void vop2_vp_handle_vblank(struct vop2_video_port *vp)
> +{
> + struct drm_crtc *crtc = &vp->crtc;
> + struct vop2 *vop2 = vp->vop2;
> + u32 val;
> +
> + guard(spinlock)(&crtc->dev->event_lock);
> +
> + if (!vp->event)
> + return;
[Severity: High]
As reported by Sashiko on RFC [3], what happens to VRR timing when a vblank
occurs without a pending vp->event?
In vop2_vp_handle_vblank(), returning early when !vp->event (and dropping the
vblank reference via drm_crtc_vblank_put() as soon as vp->event is sent)
means drm_crtc_helper_vrr_vsync() is only ever called on vblanks that complete
a page-flip event:
1. In dynamic VRR mode, if an atomic commit without DRM_MODE_PAGE_FLIP_EVENT
(such as a synchronous commit or cursor update) runs,
vop2_crtc_atomic_flush() calls drm_crtc_helper_vrr_flush() and shrinks
cur_vtotal and RK3568_VP_DSP_VTOTAL_VS_END to base_vtotal. Because
vp->event is NULL, vop2_vp_handle_vblank() returns early on the following
vblank without calling drm_crtc_helper_vrr_vsync(), leaving the display
stuck at base_vtotal instead of stretching back to max_vtotal.
2. In fixed-rate VRR mode (enabled via vrr_limiter_capable on RK3588),
drm_crtc_helper_vrr_flush() always returns false and relies on
drm_crtc_helper_vrr_vsync() running on every vblank to dither cur_vtotal
between base_vtotal and base_vtotal + 1 and accumulate cur_err. Whenever
no page-flip event is pending (or if VRR is enabled without requesting a
page-flip event), vop2_vp_handle_vblank() returns early and fixed-rate VRR
updates stop completely.
> +
> + val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
> +
> + if (!(val & BIT(vp->id))) {
> + drm_crtc_send_vblank_event(crtc, vp->event);
> + vp->event = NULL;
> + drm_crtc_vblank_put(crtc);
> +
> + if (crtc->state->vrr_enabled)
> + if (drm_crtc_helper_vrr_vsync(crtc->state))
> + vop2_vp_set_vrr_vtotal(vp, crtc->state->vrr_state.cur_vtotal);
> + }
> +}
[Severity: Medium]
Can mutating crtc->state->vrr_state in vop2_crtc_atomic_flush() and
vop2_vp_handle_vblank() race with state duplication and swap in a subsequent
atomic commit?
vop2_crtc_duplicate_state() copies crtc->state via kmemdup() under
crtc->mutex without holding crtc->dev->event_lock and before
drm_atomic_helper_setup_commit() runs:
drm_mode_atomic_ioctl()
drm_atomic_get_crtc_state()
vop2_crtc_duplicate_state()
kmemdup(to_rockchip_crtc_state(crtc->state), ...)
If a new commit duplicates crtc->state after the previous commit's
vop2_crtc_atomic_flush() set cur_vtotal = base_vtotal, but before
vop2_vp_handle_vblank() runs drm_crtc_helper_vrr_vsync() and sets
cur_vtotal = max_vtotal (and programs max_vtotal into
RK3568_VP_DSP_VTOTAL_VS_END), the duplicated new_crtc_state retains
cur_vtotal == base_vtotal and stale cur_err.
When drm_atomic_helper_swap_state() later installs that new_crtc_state as
crtc->state and vop2_crtc_atomic_flush() runs for the new commit,
drm_crtc_helper_vrr_flush() sees vrr->cur_vtotal == vrr->base_vtotal and
returns false, skipping vop2_vp_set_vrr_vtotal() while the hardware register
is still programmed to max_vtotal from the previous vblank ISR.
[ ... ]
> @@ -2635,6 +2673,9 @@ static int vop2_create_crtcs(struct vop2 *vop2)
>
> drm_crtc_attach_background_color_property(&vp->crtc);
>
> + if (vp->data->vrr_limiter_capable)
> + drm_crtc_attach_vrr_limiter_properties(&vp->crtc);
[Severity: High]
What prevents userspace from enabling VRR or changing VRR limiter properties
on a CRTC when crtc_state->vrr_state has not been validated and populated?
__drm_crtc_init_with_planes() attaches VRR_ENABLED to every atomic CRTC, and
vop2_create_crtcs() attaches the VRR limiter properties to all four RK3588
video ports regardless of the connected encoder/connector type (for example
eDP, DP, MIPI DSI, or DPI on VP3). However, crtc_state->vrr_state
(base_vtotal, max_vtotal, denom, per_frame_err) is only validated and
populated by hdmi_validate_vrr() inside drm_atomic_helper_connector_hdmi_check()
when an HDMI connector is part of the atomic state.
Neither vop2_crtc_atomic_check() nor rockchip_drm_atomic_check() checks
whether the video port and connector support VRR or whether vrr_state was
populated, and rockchip_drm_atomic_check() does not add affected connectors
when the CRTC VRR limiter properties change while vrr_enabled is already true:
1. If userspace enables fixed-rate VRR (vrr_min == vrr_max != 0) on a
non-HDMI connector on RK3588, vrr_state.base_vtotal and vrr_state.denom
remain 0. In vop2_vp_handle_vblank(), drm_crtc_helper_vrr_vsync() then
evaluates vrr->cur_err >= (int)(vrr->denom / 2) (0 >= 0), sets
vrr->cur_vtotal = vrr->base_vtotal + 1 (1), and calls
vop2_vp_set_vrr_vtotal(vp, 1), programming DSP_VTOTAL = 1 into
RK3568_VP_DSP_VTOTAL_VS_END.
2. If userspace updates the VRR limiter properties on an HDMI CRTC while
vrr_enabled is already true without a modeset, the connector is not added
to the atomic state, bypassing hdmi_validate_vrr() and VTEM infoframe
updates.
[1] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-11-2fcd7d011646@collabora.com
[2] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-11-2fcd7d011646@collabora.com
[3] https://lore.kernel.org/r/20260921-vrr-limiter-uapi-v1-11-2fcd7d011646@collabora.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=14
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 19/27] drm/connector: hdmi: Add QMS to VTEM EMP generation
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:45 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:45 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] drm/connector: hdmi: missing sink QMS capability check in VTEM generation
- [High] drm/connector: hdmi: vfront read from unpopulated adjusted_mode in VTEM
--
commit cd1865130cc65c0f62c6e87bad2b21911a9e7073
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/connector: hdmi: Add QMS to VTEM EMP generation
This commit updates hdmi_generate_emp_infoframe_vtem() to populate QMS VTEM
Extended Metadata Packet fields when conn_state->vrr_display_mode is set to
VRR_DISPLAY_MODE_MEDIA and the connector supports QMS.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> index 1e576dfbd856d..892079e41a0fb 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> @@ -890,11 +890,18 @@ static int hdmi_generate_emp_infoframe_vtem(const struct drm_connector *connecto
> break;
> case VRR_DISPLAY_MODE_OFF:
> break;
> + case VRR_DISPLAY_MODE_MEDIA:
> + if (!connector->hdmi.qms_supported)
> + return -EOPNOTSUPP;
> + vtem->qms_en = true;
> + vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
> + vtem->m_const = !vrr->dynamic;
> + break;
[Severity: High]
Does hdmi_generate_emp_infoframe_vtem() in
drivers/gpu/drm/display/drm_hdmi_state_helper.c also need to check the sink's
QMS capability in info->hdmi.vrr_cap.qms_capable?
In drivers/gpu/drm/drm_edid.c:drm_parse_hdmi_gaming_info(), sink QMS support
and Gaming-VRR support are tracked as independent flags:
vrr->qms_capable = db[8] & DRM_EDID_QMS;
if (cea_db_payload_len(db) < 9)
return;
vrr->vrr_min = db[9] & DRM_EDID_VRR_MIN_MASK;
vrr->supported = (vrr->vrr_min > 0 && vrr->vrr_min <= 48);
Earlier in hdmi_generate_emp_infoframe_vtem(), VTEM generation returns early
when Gaming-VRR is not supported by the sink:
if (!info->hdmi.vrr_cap.supported)
return 0;
When drm_atomic_helper_connector_hdmi_check() -> hdmi_generate_infoframes() ->
hdmi_generate_emp_infoframe_vtem() runs with conn_state->vrr_display_mode set
to VRR_DISPLAY_MODE_MEDIA, this leads to two issues:
1. On a sink that supports QMS (info->hdmi.vrr_cap.qms_capable is true) but
not Gaming-VRR (info->hdmi.vrr_cap.supported is false),
hdmi_generate_emp_infoframe_vtem() returns 0 early with infoframe->set = false
and never generates the QMS VTEM packet.
2. On a sink that supports Gaming-VRR (info->hdmi.vrr_cap.supported is true)
but not QMS (info->hdmi.vrr_cap.qms_capable is false), the
VRR_DISPLAY_MODE_MEDIA case only checks source-side
connector->hdmi.qms_supported. Because vrr_display_mode can be set when
crtc_state->vrr_enabled is false (where hdmi_validate_vrr() returns 0 early),
selecting VRR_DISPLAY_MODE_MEDIA sets vtem->qms_en = true and transmits QMS
VTEM packets to a sink that does not support QMS.
Should the early check allow either info->hdmi.vrr_cap.supported or
info->hdmi.vrr_cap.qms_capable, and the VRR_DISPLAY_MODE_GAME and
VRR_DISPLAY_MODE_MEDIA cases verify their respective sink capability flags?
> default:
> return -EINVAL;
> }
>
> - if (vtem->game_vrr_en && !avi_vic) {
> + if ((vtem->game_vrr_en || vtem->qms_en) && !avi_vic) {
> vtem->base_refresh_rate = drm_mode_vrefresh(&crtc_state->mode);
> vfront = crtc_state->adjusted_mode.crtc_vsync_start -
> crtc_state->adjusted_mode.crtc_vdisplay;
[Severity: High]
When (vtem->game_vrr_en || vtem->qms_en) && !avi_vic is true in
drivers/gpu/drm/display/drm_hdmi_state_helper.c:hdmi_generate_emp_infoframe_vtem(),
is crtc_state->adjusted_mode guaranteed to be populated yet?
In drivers/gpu/drm/drm_atomic_helper.c:drm_atomic_helper_check_modeset(), the
connector atomic_check callback
(drm_atomic_helper_connector_hdmi_check() -> hdmi_generate_infoframes() ->
hdmi_generate_emp_infoframe_vtem()) runs before mode_fixup() copies
new_crtc_state->mode into new_crtc_state->adjusted_mode:
for_each_new_crtc_in_state(state, crtc, new_crtc_state, i) {
if (!new_crtc_state->mode_changed &&
!new_crtc_state->connectors_changed)
continue;
drm_mode_copy(&new_crtc_state->adjusted_mode, &new_crtc_state->mode);
}
As a result, during an initial modeset on a non-CEA mode (!avi_vic),
crtc_state->adjusted_mode is still zeroed and vfront evaluates to 0, while on
a mode change crtc_state->adjusted_mode still contains the previous mode's
timings.
Note that hdmi_validate_vrr() in the same file computes vfront from
crtc_state->mode:
vfront = mode->crtc_vsync_start - mode->crtc_vdisplay;
if (vfront < 0 || vfront > U8_MAX) {
drm_dbg_kms(dev, "Vfront of %d would not fit in VTEM packet\n", vfront);
return -EINVAL;
}
Should hdmi_generate_emp_infoframe_vtem() also read crtc_vsync_start and
crtc_vdisplay from crtc_state->mode (matching base_refresh_rate just above
it and hdmi_validate_vrr())?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=19
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 20/27] drm/connector: hdmi: Add QMS state validation and computation
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:53 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:53 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] drm/connector: hdmi: NULL old_conn_state->crtc dereference in hdmi_validate_vrr()
- [Medium] drm/connector: hdmi: QMS rates rejected in hdmi_validate_qms() when vrr_max is 0
- [Medium] drm/connector: hdmi: mismatched QMS checks in hdmi_validate_vrr() and VTEM path
- [Medium] drm/connector: hdmi: broken vrr_state->dynamic tracking in hdmi_validate_vrr()
- [Medium] drm/connector: hdmi: unhandled VRR_DISPLAY_MODE_OFF and ungated cinema_vrr check
- [Low] drm/connector: hdmi: missing @conn_state in hdmi_validate_qms() kernel-doc
--
commit ca0c861772300673280d14b0f18ffdb23766fd49
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/connector: hdmi: Add QMS state validation and computation
This commit adds validation and state computation for HDMI Quick Media
Switching to hdmi_validate_vrr() and introduces hdmi_validate_qms() to
verify target frame rates and compute the signaling byte.
> diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> index 892079e41a0fb..ef08a72e33f3e 100644
> --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c
> +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c
[ ... ]
> @@ -992,6 +1013,66 @@ static int cmp_fraction(u32 a, u32 b, u32 c, u32 d)
> return 0;
> }
>
> +/**
> + * hdmi_validate_qms - Validate HDMI QMS state
> + * @crtc_state: pointer to a &struct drm_crtc_state to run validation on
> + * @info: pointer to a &struct drm_display_info of the accompanying connector
> + *
> + * Validate that the drm_crtc_vrr_state.vrr_min_numerator and
> + * drm_crtc_vrr_state.vrr_min_denominator are acceptable for HDMI QMS, and check
> + * that the display is HDMI QMS capable.
> + *
> + * Returns:
> + * - Negative errno on failure
> + * - the corresponding QMS signaling byte for the target frame rate otherwise
> + */
> +static int hdmi_validate_qms(struct drm_crtc_state *crtc_state,
> + struct drm_connector_state *conn_state,
> + const struct drm_display_info *info)
[Severity: Low]
This isn't a bug, but the kernel-doc comment for hdmi_validate_qms() omits
the @conn_state parameter, which triggers a kernel-doc warning when building
with W=1. Also, the comment body refers to vrr_min_numerator and
vrr_min_denominator instead of the struct drm_crtc_vrr_state fields
vrr_min_n and vrr_min_d.
> +{
> + u32 rate_n = crtc_state->vrr_state.vrr_min_n;
> + u32 rate_d = crtc_state->vrr_state.vrr_min_d;
> + u32 min_rate_n, max_rate;
> + u32 g, i;
> +
> + if (conn_state->vrr_display_mode != VRR_DISPLAY_MODE_MEDIA)
> + return 0;
> +
> + if (!drm_crtc_helper_vrr_is_fixed_rate(&crtc_state->vrr_state))
> + return -EINVAL;
> +
> + if (info->hdmi.vrr_cap.qms_tfr_min)
> + min_rate_n = 24000;
> + else
> + min_rate_n = info->hdmi.vrr_cap.vrr_min * 1000;
> +
> + if (info->hdmi.vrr_cap.qms_tfr_max)
> + max_rate = info->hdmi.vrr_cap.vrr_max;
> + else
> + max_rate = 60;
> +
> + g = gcd(rate_n, rate_d);
> + if (g != 1) {
> + rate_n /= g;
> + rate_d /= g;
> + }
> +
> + if (!rate_d)
> + return -EINVAL;
> +
> + for (i = 0; i < ARRAY_SIZE(hdmi_qms_tfrs); i++) {
> + if (cmp_fraction(min_rate_n, 1001, hdmi_qms_tfrs[i].rate_n,
> + hdmi_qms_tfrs[i].rate_d) > 0)
> + continue;
> + if (rate_n == hdmi_qms_tfrs[i].rate_n &&
> + rate_d == hdmi_qms_tfrs[i].rate_d &&
> + cmp_fraction(rate_n, rate_d, max_rate, 1) <= 0)
> + return hdmi_qms_tfrs[i].tfr_byte;
> + }
> +
> + return -EINVAL;
> +}
[Severity: Medium]
What happens in hdmi_validate_qms() when info->hdmi.vrr_cap.qms_tfr_max is
true, but info->hdmi.vrr_cap.vrr_max is 0?
In include/drm/drm_connector.h, struct drm_hdmi_vrr_cap documents that
vrr_max == 0 is a valid value (and occurs on sinks that only support media
switching without game variable refresh rate), and that qms_tfr_min and
qms_tfr_max fall back to monitor_range:
/**
* @vrr_max : maximum supported variable refresh rate in Hz (optional).
* Valid values are either 0 (max based on video mode) or >= 100
*/
u16 vrr_max;
...
/**
* @qms_tfr_min: If set, the sink uses 24/1.001Hz as its minimum rate
* for QMS. If unset, @drm_display_info.monitor_range.min_vfreq is used.
*/
bool qms_tfr_min;
/**
* @qms_tfr_max: If set, @drm_display_info.monitor_range.max_vfreq is
* used by the sink as its maximum rate for QMS. If unset, 60Hz is used.
*/
bool qms_tfr_max;
When qms_tfr_max is set and vrr_max is 0, max_rate becomes 0 in
hdmi_validate_qms(), so cmp_fraction(rate_n, rate_d, max_rate, 1) <= 0
evaluates to false for every entry in hdmi_qms_tfrs[] and rejects all target
frame rates.
Similarly, on a sink that supports media switching without game variable
refresh rate (where vrr_min is 0) and has qms_tfr_min unset, neither
hdmi_validate_qms() nor hdmi_validate_vrr() consults
info->monitor_range.min_vfreq, leaving min_vfreq_n as 0 and failing validation
at if (!min_vfreq_n) in hdmi_validate_vrr().
Should hdmi_validate_qms() and hdmi_validate_vrr() fall back to
info->monitor_range.min_vfreq and info->monitor_range.max_vfreq (or
mode_refresh) when vrr_min or vrr_max is 0?
[ ... ]
> @@ -1129,8 +1210,17 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
> if (old_conn_state)
> old_crtc_state = drm_atomic_get_old_crtc_state(state, old_conn_state->crtc);
[Severity: High]
Can old_conn_state->crtc be NULL here when a previously disabled connector is
enabled in the same atomic commit that enables variable refresh rate on the
crtc?
In include/drm/drm_atomic.h, drm_atomic_get_old_crtc_state() indexes
state->crtcs using drm_crtc_index(crtc) without checking if crtc is NULL:
static inline struct drm_crtc_state *
drm_atomic_get_old_crtc_state(const struct drm_atomic_commit *state,
struct drm_crtc *crtc)
{
return state->crtcs[drm_crtc_index(crtc)].old_state;
}
Should this check if (old_conn_state && old_conn_state->crtc) before calling
drm_atomic_get_old_crtc_state()?
> - if (!info->hdmi.vrr_cap.supported)
> - return -EOPNOTSUPP;
> + switch (new_conn_state->vrr_display_mode) {
> + case VRR_DISPLAY_MODE_GAME:
> + if (!info->hdmi.vrr_cap.supported)
> + return -EOPNOTSUPP;
> + break;
> + case VRR_DISPLAY_MODE_MEDIA:
> + if (!info->hdmi.vrr_cap.qms_capable ||
> + !connector->hdmi.qms_supported)
> + return -EOPNOTSUPP;
> + break;
> + }
[Severity: Medium]
How does this capability check interact with
hdmi_generate_emp_infoframe_vtem()?
In drivers/gpu/drm/display/drm_hdmi_state_helper.c,
hdmi_generate_emp_infoframe_vtem() still returns early when
!info->hdmi.vrr_cap.supported:
if (!info->hdmi.vrr_cap.supported)
return 0;
...
case VRR_DISPLAY_MODE_MEDIA:
if (!connector->hdmi.qms_supported)
return -EOPNOTSUPP;
vtem->qms_en = true;
vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
vtem->m_const = !vrr->dynamic;
break;
On a sink where info->hdmi.vrr_cap.qms_capable is true but
info->hdmi.vrr_cap.supported is false, hdmi_validate_vrr() now accepts the
commit, but hdmi_generate_emp_infoframe_vtem() skips generating the infoframe.
Conversely, when new_crtc_state->vrr_enabled is false and vrr_display_mode is
set to media mode, hdmi_validate_vrr() returns 0 early at:
if (!new_crtc_state->vrr_enabled)
return 0;
without checking info->hdmi.vrr_cap.qms_capable or updating
new_conn_state->hdmi.qms_tfr_byte, while hdmi_generate_emp_infoframe_vtem()
still runs and sets vtem->qms_en = true with an unvalidated 0 or stale
qms_tfr_byte from a previous commit.
Should hdmi_generate_emp_infoframe_vtem() and hdmi_validate_vrr() check the
same conditions for media mode and crtc_state->vrr_enabled?
> mode_refresh = drm_mode_vrefresh(mode);
>
> @@ -1145,7 +1235,10 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
> max_vfreq_n = mode_refresh;
>
> is_fixed = drm_crtc_helper_vrr_is_fixed_rate(vrr_state);
> - if (is_fixed && info->hdmi.vrr_cap.cinema_vrr)
> + if (is_fixed && ((info->hdmi.vrr_cap.qms_tfr_min &&
> + new_conn_state->vrr_display_mode == VRR_DISPLAY_MODE_MEDIA &&
> + connector->hdmi.qms_supported && info->hdmi.vrr_cap.qms_capable) ||
> + info->hdmi.vrr_cap.cinema_vrr))
> min_vfreq_n = 24;
[Severity: Medium]
Are there two mode-selection issues here in hdmi_validate_vrr()?
First, in the switch on new_conn_state->vrr_display_mode above, neither
VRR_DISPLAY_MODE_OFF nor a default case is handled. When
new_crtc_state->vrr_enabled is true and vrr_display_mode is
VRR_DISPLAY_MODE_OFF, hdmi_validate_vrr() no longer checks sink capability and
configures crtc vtotal adjustment even though
hdmi_generate_emp_infoframe_vtem() leaves vtem->game_vrr_en and vtem->qms_en
disabled.
Second, in the min_vfreq_n check above, info->hdmi.vrr_cap.cinema_vrr is not
restricted to VRR_DISPLAY_MODE_GAME. If vrr_display_mode is
VRR_DISPLAY_MODE_MEDIA on a sink where cinema_vrr is true and qms_tfr_min is
false, min_vfreq_n will still be lowered to 24 Hz.
Should VRR_DISPLAY_MODE_OFF be rejected (or handled) when vrr_enabled is true,
and should info->hdmi.vrr_cap.cinema_vrr only apply when vrr_display_mode is
VRR_DISPLAY_MODE_GAME?
>
> /* If the VRR target changed, make sure to reset the error accumulator */
> @@ -1176,6 +1269,19 @@ static int hdmi_validate_vrr(struct drm_connector *connector,
> return ret;
> }
>
> + ret = hdmi_validate_qms(new_crtc_state, new_conn_state, info);
> + if (ret < 0) {
> + drm_dbg_kms(dev, "Invalid QMS rate: %pe\n", ERR_PTR(ret));
> + return ret;
> + }
> +
> + new_conn_state->hdmi.qms_tfr_byte = ret;
> + if (!old_conn_state ||
> + new_conn_state->hdmi.qms_tfr_byte != old_conn_state->hdmi.qms_tfr_byte)
> + vrr_state->dynamic = true;
> + else
> + vrr_state->dynamic = false;
[Severity: Medium]
Does setting vrr_state->dynamic purely from whether qms_tfr_byte changed work
as expected for vtem->m_const in both media and game modes?
In hdmi_generate_emp_infoframe_vtem(), vtem->m_const is set from
!vrr->dynamic:
vtem->qms_en = true;
vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
vtem->m_const = !vrr->dynamic;
When media mode is first enabled or its target rate changes, qms_tfr_byte
differs from old_conn_state->hdmi.qms_tfr_byte, so vrr_state->dynamic is set
to true and the infoframe is generated with m_const = false. Because the
infoframe is only generated during atomic_check and is not updated after the
rate transition completes, won't m_const remain 0 on all subsequent frames of
constant-rate playback unless another atomic commit is made with the same
target rate?
In addition, when vrr_display_mode is VRR_DISPLAY_MODE_GAME with a fixed target
rate (is_fixed == true), hdmi_validate_qms() returns 0, so both old and new
qms_tfr_byte are 0. This check then sets vrr_state->dynamic = false even when
the fixed target rate (vrr_min_n / vrr_min_d) changed in the commit.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=20
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 23/27] drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 17:57 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 17:57 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] drm/tests: double ctx cleanup in drm_test_check_hdmi_vrr_target_rate_fail()
--
commit 75a8664a7bf8d42311eb56958c313fafe87f6d41
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/tests: hdmi: Add Fixed/Constrained rate VRR tests
This commit adds KUnit tests for HDMI VRR fixed and constrained target rates,
including CinemaVRR and invalid target rate checks.
> diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
> index 09ce304210892..7da26b3713d8c 100644
> --- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
> +++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
> @@ -3673,10 +3673,396 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
[ ... ]
> +static void drm_test_check_hdmi_vrr_target_rate_fail(struct kunit *test)
> +{
> + const struct vrr_target_error_params *params = test->param_value;
> + struct drm_atomic_helper_connector_hdmi_priv *priv;
> + struct drm_modeset_acquire_ctx ctx;
> + struct drm_display_mode *preferred;
> + struct drm_crtc_state *crtc_state;
> + struct drm_atomic_commit *state;
> + struct drm_connector *conn;
> + int ret;
> +
> + priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
> + BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
> + 8,
> + &dummy_connector_hdmi_funcs,
> + test_edid_hdmi_vrr);
> + KUNIT_ASSERT_NOT_NULL(test, priv);
> +
> + conn = &priv->connector;
> + KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
> + KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
> + KUNIT_ASSERT_LT(test, 24, conn->display_info.hdmi.vrr_cap.vrr_min);
> +
> + preferred = find_preferred_mode(conn);
> + KUNIT_ASSERT_NOT_NULL(test, preferred);
> +
> + guard(modeset_lock)(&ctx);
> +
> + allocate_state_for_vrr_test(test, priv, &ctx, preferred, &state);
> +
> +retry:
> + prepare_state_for_vrr_test(test, priv, &ctx, state, &crtc_state);
> +
> + /*
> + * Allow individual test parameters to play with the CinemaVRR flag
> + * without needing to bring their own modified EDID.
> + */
> + conn->display_info.hdmi.vrr_cap.cinema_vrr = params->cinema_vrr_override;
> +
> + crtc_state->vrr_enabled = true;
> + crtc_state->vrr_state.vrr_min_n = params->vrr_min_n;
> + crtc_state->vrr_state.vrr_min_d = params->vrr_min_d;
> + crtc_state->vrr_state.vrr_max_n = params->vrr_max_n;
> + crtc_state->vrr_state.vrr_max_d = params->vrr_max_d;
> +
> + ret = drm_atomic_commit(state);
> + if (ret == -EDEADLK) {
> + drm_atomic_commit_clear(state);
> + ret = drm_modeset_backoff(&ctx);
> + if (!ret)
> + goto retry;
> + }
> + KUNIT_EXPECT_LT(test, ret, 0);
> +
> + drm_modeset_drop_locks(&ctx);
> + drm_modeset_acquire_fini(&ctx);
> +}
[Severity: Medium]
Since guard(modeset_lock)(&ctx) is used earlier in
drm_test_check_hdmi_vrr_target_rate_fail(), drop_fini_modeset(&ctx) will
automatically run when the function exits:
static void drop_fini_modeset(struct drm_modeset_acquire_ctx *ctx)
{
drm_modeset_drop_locks(ctx);
drm_modeset_acquire_fini(ctx);
}
DEFINE_GUARD(modeset_lock, struct drm_modeset_acquire_ctx *,
drm_modeset_acquire_init(_T, 0), drop_fini_modeset(_T));
Won't calling drm_modeset_drop_locks(&ctx) and drm_modeset_acquire_fini(&ctx)
explicitly at the end of the function cause ctx to be finalized twice via
ww_acquire_fini(), triggering lockdep or DEBUG_WW_MUTEXES warnings?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=23
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 21/27] drm/rockchip: dw_hdmi_qp: Add QMS support
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 18:01 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 18:01 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] drm/rockchip: dw_hdmi_qp: QMS rate changes do not update VTEM EMP or CRTC
- [High] drm/rockchip: dw_hdmi_qp: broken QMS-only sink and VTEM EMP validation
- [High] drm/rockchip: gated VRR helpers in vop2_vp_handle_vblank() break QMS vtotal
--
commit fc51237acec11aa5e7ba522a20138aec70453297
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/rockchip: dw_hdmi_qp: Add QMS support
This commit enables Quick Media Switching (QMS) on Rockchip DW HDMI QP
connectors by adding VRR_DISPLAY_MODE_MEDIA to RK_VRR_DISPLAY_MODES and
setting connector->hdmi.qms_supported to true.
> diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
> index dcde50bca36bf..215cf746dc41c 100644
> --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
> +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c
> @@ -90,7 +90,8 @@
> #define RK_COLOR_FMT_YUV420 0x3
>
> #define RK_VRR_DISPLAY_MODES (BIT(VRR_DISPLAY_MODE_GAME) | \
> - BIT(VRR_DISPLAY_MODE_OFF))
> + BIT(VRR_DISPLAY_MODE_OFF) | \
> + BIT(VRR_DISPLAY_MODE_MEDIA))
>
> struct rockchip_hdmi_qp {
> struct device *dev;
> @@ -753,6 +754,8 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master,
> return dev_err_probe(hdmi->dev, ret,
> "Failed to attach 'vrr_display_mode' property\n");
>
> + connector->hdmi.qms_supported = true;
[Severity: High]
Does enabling QMS here work when switching QMS target frame rates or
updating vrr_display_mode without a full modeset?
When userspace changes CRTC VRR target frame rate limits
(VRR_MIN_NUMERATOR, VRR_MIN_DENOMINATOR, VRR_MAX_NUMERATOR,
VRR_MAX_DENOMINATOR) while vrr_enabled remains true,
rockchip_drm_atomic_check() only checks whether vrr_enabled toggled:
drivers/gpu/drm/rockchip/rockchip_drm_fb.c:rockchip_drm_atomic_check() {
...
if (old_crtc_state->vrr_enabled != new_crtc_state->vrr_enabled) {
new_crtc_state->mode_changed = true;
...
}
Because drm_atomic_crtc_needs_modeset() remains false,
drm_atomic_helper_check_modeset() does not call
drm_atomic_add_affected_connectors(), so
drm_bridge_connector_atomic_check() ->
drm_atomic_helper_connector_hdmi_check() -> hdmi_validate_vrr() is never
run. As a result, hdmi_validate_qms(), hdmi_validate_vrr_limits(), and
hdmi_generate_emp_infoframe_vtem() are skipped.
Even when the connector is included in a non-modeset commit, dw-hdmi-qp
only calls drm_atomic_helper_connector_hdmi_update_infoframes() inside
dw_hdmi_qp_bridge_atomic_enable():
drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c:dw_hdmi_qp_bridge_atomic_enable() {
...
drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
}
Consequently, dw_hdmi_qp_bridge_write_vtem_emp() is not called outside a
full modeset, leaving the PKT_EMP_VTEM_CONTENTS0..7 registers with stale
QMS_EN, Next_TFR, and M_CONST values. In addition, after the initial
modeset where old_conn_state->hdmi.qms_tfr_byte !=
new_conn_state->hdmi.qms_tfr_byte sets vrr_state->dynamic = true and
vtem->m_const = 0, subsequent commits at that constant QMS rate never
update the hardware VTEM EMP to transition M_CONST to 1.
[Severity: High]
How are QMS-only HDMI sinks and VTEM EMP validation handled when
VRR_DISPLAY_MODE_MEDIA is used?
First, an HDMI sink can support QMS (info->hdmi.vrr_cap.qms_capable ==
true) without supporting continuous Gaming-VRR
(info->hdmi.vrr_cap.supported == false). However,
hdmi_generate_emp_infoframe_vtem() returns early if vrr_cap.supported is
false:
drivers/gpu/drm/display/drm_hdmi_state_helper.c:hdmi_generate_emp_infoframe_vtem() {
...
if (!info->hdmi.vrr_cap.supported)
return 0;
...
switch (conn_state->vrr_display_mode) {
...
case VRR_DISPLAY_MODE_MEDIA:
if (!connector->hdmi.qms_supported)
return -EOPNOTSUPP;
vtem->qms_en = true;
vtem->next_tfr = conn_state->hdmi.qms_tfr_byte;
vtem->m_const = !vrr->dynamic;
break;
}
Similarly, dw_hdmi_qp_rockchip_encoder_atomic_check() sets the connector's
vrr_capable property using drm_display_info_is_vrr_capable(), which only
checks info->hdmi.vrr_cap.supported and ignores
info->hdmi.vrr_cap.qms_capable. Won't QMS-only sinks report vrr_capable as
false and fail to generate VTEM EMPs?
Second, when crtc_state->vrr_enabled is false, hdmi_validate_vrr() returns
0 early, but hdmi_generate_emp_infoframe_vtem() still runs without
checking crtc_state->vrr_enabled or info->hdmi.vrr_cap.qms_capable. If
conn_state->vrr_display_mode is VRR_DISPLAY_MODE_MEDIA on a sink where
vrr_cap.supported is true, won't it emit a VTEM EMP with qms_en = true and
a zero or stale qms_tfr_byte even when VRR is disabled or the sink does
not support QMS?
Third, in hdmi_validate_qms():
drivers/gpu/drm/display/drm_hdmi_state_helper.c:hdmi_validate_qms() {
...
if (info->hdmi.vrr_cap.qms_tfr_max)
max_rate = info->hdmi.vrr_cap.vrr_max;
else
max_rate = 60;
}
If a sink advertises qms_tfr_max = true with vrr_max = 0 (indicating no
HF-VSDB upper limit below the mode refresh rate), max_rate becomes 0 and
cmp_fraction(rate_n, rate_d, max_rate, 1) <= 0 rejects every QMS target
frame rate.
[Severity: High]
Will VOP2 vtotal updates in vop2_crtc_atomic_flush() and
vop2_vp_handle_vblank() work properly for fixed-rate QMS modes?
In hdmi_validate_qms(), VRR_DISPLAY_MODE_MEDIA requires
drm_crtc_helper_vrr_is_fixed_rate(&crtc_state->vrr_state) to be true. In
drm_crtc_helper_vrr_flush(), fixed-rate VRR unconditionally returns false:
drivers/gpu/drm/drm_crtc_helper.c:drm_crtc_helper_vrr_flush() {
...
if (drm_crtc_helper_vrr_is_fixed_rate(vrr))
return false;
}
Because vop2_crtc_atomic_flush() only calls vop2_vp_set_vrr_vtotal() when
drm_crtc_helper_vrr_flush() returns true, the initial frame in QMS mode is
not programmed at flush time.
Instead, fixed-rate cur_vtotal updates and fractional QMS frame-rate
dithering (such as 24000/1001 or 60000/1001 Hz) only happen in
drm_crtc_helper_vrr_vsync(). However, vop2_vp_handle_vblank() only calls
drm_crtc_helper_vrr_vsync() when vp->event is pending:
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c:vop2_vp_handle_vblank() {
if (!vp->event)
return;
val = vop2_readl(vop2, RK3568_REG_CFG_DONE);
if (!(val & BIT(vp->id))) {
drm_crtc_send_vblank_event(crtc, vp->event);
vp->event = NULL;
drm_crtc_vblank_put(crtc);
if (crtc->state->vrr_enabled)
if (drm_crtc_helper_vrr_vsync(crtc->state))
vop2_vp_set_vrr_vtotal(vp, crtc->state->vrr_state.cur_vtotal);
}
}
Once the commit event is sent and drm_crtc_vblank_put() is called,
subsequent vblanks without a new atomic commit will return early at
!vp->event, stopping fractional vtotal dithering between base_vtotal and
base_vtotal + 1.
Furthermore, because both vop2_crtc_atomic_flush() and
vop2_vp_handle_vblank() guard the VRR helper calls with
if (crtc_state->vrr_enabled), the !crtc_state->vrr_enabled paths in
drm_crtc_helper_vrr_flush() and drm_crtc_helper_vrr_vsync() that reset
cur_vtotal and cur_err to 0 are never reached when VRR is disabled. If QMS
is disabled (resetting RK3568_VP_DSP_VTOTAL_VS_END to mode->crtc_vtotal
during modeset) and later re-enabled at the same integer-divisible target
rate, cur_vtotal already matches base_vtotal, so
drm_crtc_helper_vrr_vsync() returns false and vop2_vp_set_vrr_vtotal() is
never called.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=21
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 27/27] drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 18:03 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 18:03 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] drm/tests: WARN_ON() in VRR limit tests via drm_object_property_get_value()
--
commit 1a4f6d1ae4304b42e44e6989719d4059e858153e
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/tests: hdmi: Add tests for vrr_display_{min,max} properties
This commit attaches the vrr_display_min and vrr_display_max properties to
the KUnit HDMI state helper test connector and adds two tests verifying
drm_connector_hdmi_update_vrr_limits().
> diff --git a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
> index 95a922e7f2515..3f94133b20b10 100644
> --- a/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
> +++ b/drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c
[ ... ]
> @@ -3675,6 +3678,88 @@ static void drm_test_check_hdmi_vrr_flush_vsync(struct kunit *test)
> crtc_state->vrr_state.max_vtotal);
> }
>
> +/*
> + * Check that on a VRR-capable sink, the HDMI VRR limits helper sets the
> + * display_min/display_max connector properties to the right values.
> + */
> +static void drm_test_check_hdmi_vrr_display_limits(struct kunit *test)
> +{
> + struct drm_atomic_helper_connector_hdmi_priv *priv;
> + struct drm_display_mode *preferred;
> + struct drm_connector *conn;
> + u64 disp_min, disp_max;
> + int ret;
> +
> + priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
> + BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
> + 8,
> + &dummy_connector_hdmi_funcs,
> + test_edid_hdmi_vrr);
> + KUNIT_ASSERT_NOT_NULL(test, priv);
> +
> + conn = &priv->connector;
> + KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
> + KUNIT_ASSERT_TRUE(test, conn->display_info.hdmi.vrr_cap.supported);
> +
> + preferred = find_preferred_mode(conn);
> + KUNIT_ASSERT_NOT_NULL(test, preferred);
> +
> + ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
> + &disp_min);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> +
> + ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
> + &disp_max);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> +
> + KUNIT_EXPECT_EQ(test, disp_min, 30);
> + KUNIT_EXPECT_EQ(test, disp_max, 120);
> +}
> +
> +/*
> + * Check that on a non-VRR-capable sink, the HDMI VRR limits helper sets the
> + * display_min/display_max connector properties to 0.
> + */
> +static void drm_test_check_hdmi_vrr_display_limits_zero(struct kunit *test)
> +{
> + struct drm_atomic_helper_connector_hdmi_priv *priv;
> + struct drm_display_mode *preferred;
> + struct drm_connector *conn;
> + u64 disp_min, disp_max;
> + int ret;
> +
> + priv = drm_kunit_helper_connector_hdmi_init_with_edid_funcs(test,
> + BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444),
> + 8,
> + &dummy_connector_hdmi_funcs,
> + test_edid_hdmi_4k_rgb_yuv420_dc_max_340mhz);
> + KUNIT_ASSERT_NOT_NULL(test, priv);
> +
> + conn = &priv->connector;
> + KUNIT_ASSERT_TRUE(test, conn->display_info.is_hdmi);
> + KUNIT_ASSERT_FALSE(test, conn->display_info.hdmi.vrr_cap.supported);
> +
> + preferred = find_preferred_mode(conn);
> + KUNIT_ASSERT_NOT_NULL(test, preferred);
> +
> + ret = drm_connector_hdmi_update_vrr_limits(conn, preferred);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + ret = drm_object_property_get_value(&conn->base, conn->vrr_display_min_property,
> + &disp_min);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> +
> + ret = drm_object_property_get_value(&conn->base, conn->vrr_display_max_property,
> + &disp_max);
[Severity: Medium]
Will calling drm_object_property_get_value() in
drm_test_check_hdmi_vrr_display_limits() and
drm_test_check_hdmi_vrr_display_limits_zero() trigger a WARN_ON() when
running these tests?
Both tests initialize the test DRM device through
drm_kunit_helper_connector_hdmi_init_with_edid_funcs() ->
__connector_hdmi_init(), which allocates the device with DRIVER_ATOMIC:
drivers/gpu/drm/tests/drm_hdmi_state_helper_test.c:__connector_hdmi_init() {
...
priv = drm_kunit_helper_alloc_drm_device(test, dev,
struct drm_atomic_helper_connector_hdmi_priv, drm,
DRIVER_MODESET | DRIVER_ATOMIC);
...
}
Unlike drm_object_property_set_value(), drm_object_property_get_value() does
not exempt DRM_MODE_PROP_IMMUTABLE properties and unconditionally warns on
atomic devices:
drivers/gpu/drm/drm_mode_object.c:
int drm_object_property_get_value(struct drm_mode_object *obj,
struct drm_property *property, uint64_t *val)
{
WARN_ON(drm_drv_uses_atomic_modeset(property->dev));
return __drm_object_property_get_value(obj, property, val);
}
Since vrr_display_min_property and vrr_display_max_property are created with
DRM_MODE_PROP_IMMUTABLE, should both tests use
drm_object_immutable_property_get_value() instead?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=27
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v2 26/27] drm/debugfs: Expose HDMI VRR in vrr_range debugfs
2026-11-05 15:52 ` Nicolas Frattaroli
(?)
@ 2026-11-05 18:05 ` sashiko-bot
-1 siblings, 0 replies; 74+ messages in thread
From: sashiko-bot @ 2026-11-05 18:05 UTC (permalink / raw)
To: Nicolas Frattaroli; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] drm/debugfs: vrr_range_show() reports Max: 0 when HDMI VRRmax is 0
--
commit c20f012be0daea32ab333f3e4f526e77c8606b3b
Author: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
drm/debugfs: Expose HDMI VRR in vrr_range debugfs
This commit updates vrr_range_show() to report HDMI VRR limits from
display_info.hdmi.vrr_cap when HDMI VRR is supported, and protects access
to connector->status and connector->display_info with mode_config.mutex.
> diff --git a/drivers/gpu/drm/drm_debugfs.c b/drivers/gpu/drm/drm_debugfs.c
> index a72f9723d3a97..476f44642ac3e 100644
> --- a/drivers/gpu/drm/drm_debugfs.c
> +++ b/drivers/gpu/drm/drm_debugfs.c
> @@ -586,12 +586,26 @@ static ssize_t edid_write(struct file *file, const char __user *ubuf,
> static int vrr_range_show(struct seq_file *m, void *data)
> {
> struct drm_connector *connector = m->private;
> + u16 min_vfreq, max_vfreq;
> +
> + scoped_cond_guard(mutex_intr, return -EINTR,
> + &connector->dev->mode_config.mutex) {
> + struct drm_display_info *info = &connector->display_info;
> +
> + if (connector->status != connector_status_connected)
> + return -ENODEV;
> +
> + if (info->is_hdmi && info->hdmi.vrr_cap.supported) {
> + min_vfreq = info->hdmi.vrr_cap.vrr_min;
> + max_vfreq = info->hdmi.vrr_cap.vrr_max;
> + } else {
> + min_vfreq = info->monitor_range.min_vfreq;
> + max_vfreq = info->monitor_range.max_vfreq;
> + }
[Severity: Medium]
Can vrr_range_show() in drivers/gpu/drm/drm_debugfs.c report "Max: 0" when
an HDMI 2.1 sink advertises VRR support with VRRmax set to 0 in its HF-VSDB?
In drivers/gpu/drm/drm_edid.c, drm_parse_hdmi_gaming_info() sets
vrr->supported = true even when vrr->vrr_max is 0 (either because byte 10
of the HF-VSDB is omitted or because the 10-bit VRRmax field is 0, which
HDMI 2.1 uses when the upper refresh limit is below 100 Hz or defined by
the video mode / EDID monitor range):
vrr->vrr_min = db[9] & DRM_EDID_VRR_MIN_MASK;
vrr->supported = (vrr->vrr_min > 0 && vrr->vrr_min <= 48);
if (cea_db_payload_len(db) < 10)
return;
vrr->vrr_max = (db[9] & DRM_EDID_VRR_MAX_UPPER_MASK) << 2 | db[10];
vrr->supported &= (vrr->vrr_max == 0 || vrr->vrr_max >= 100);
Drivers such as amdgpu handle vrr_cap.vrr_max == 0 in
amdgpu_dm_update_freesync_caps() in
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c by falling back
to connector->display_info.monitor_range.max_vfreq (or the highest mode
refresh rate, AMD FreeSync VSDB, and panel quirks) and storing the resolved
bounds back into connector->display_info.monitor_range:
if (vrr_cap->supported && vrr_cap->vrr_min > 0) {
amdgpu_dm_connector->min_vfreq = vrr_cap->vrr_min;
amdgpu_dm_connector->max_vfreq = vrr_cap->vrr_max ?
vrr_cap->vrr_max :
connector->display_info.monitor_range.max_vfreq;
By checking info->is_hdmi && info->hdmi.vrr_cap.supported first and
unconditionally reading info->hdmi.vrr_cap.vrr_max, won't vrr_range_show()
bypass info->monitor_range and output "Max: 0" whenever VRRmax is 0?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105-vrr-limiter-uapi-v2-0-55f4215be2c2@collabora.com?part=26
^ permalink raw reply [flat|nested] 74+ messages in thread
end of thread, other threads:[~2026-11-05 18:05 UTC | newest]
Thread overview: 74+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 15:51 [PATCH v2 00/27] VRR Target Rate Limiter KMS uAPI and Implementation Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 15:51 ` [PATCH v2 01/27] drm: Add VRR state Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:44 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 02/27] drm/connector: add vrr_display_mode connector property Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:27 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 03/27] video/hdmi: Add VTEM EMP packing Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:29 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 04/27] drm/bridge: Add VTEM EMP support Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:33 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 05/27] drm/connector: hdmi: Add VTEM EMP generation Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:53 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 06/27] drm/crtc-helper: Add VRR helper functions Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:49 ` sashiko-bot
2026-11-05 15:51 ` [PATCH v2 07/27] drm/bridge: synopsys: Add VTEM EMP support Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 15:51 ` [PATCH v2 08/27] drm/connector: Add drm_display_info_is_vrr_capable Nicolas Frattaroli
2026-11-05 15:51 ` Nicolas Frattaroli
2026-11-05 16:58 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 09/27] drm: Add VRR target frame rate properties Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:07 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 10/27] drm/connector: Add vrr_display_{min,max} connector properties Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:08 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 11/27] drm: Implement VRR rate limiting Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:17 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 12/27] drm/rockchip: dw_hdmi_qp: Add VRR support Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:30 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 13/27] drm/rockchip: Set mode_changed on vrr_enabled change Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 14/27] drm/rockchip: vop2: Enable VRR Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:40 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 15/27] drm/edid: Parse QMS flag from HDMI SCDS Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 16/27] drm/edid: Parse QMS TFR min/max flags " Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 17/27] drm/connector: Add "Media" VRR display mode for QMS Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 18/27] video/hdmi: Add support for QMS in VTEM EMP packing Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 19/27] drm/connector: hdmi: Add QMS to VTEM EMP generation Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:45 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 20/27] drm/connector: hdmi: Add QMS state validation and computation Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:53 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 21/27] drm/rockchip: dw_hdmi_qp: Add QMS support Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 18:01 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 22/27] drm/tests: hdmi: Add "Game Mode" VRR tests Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 23/27] drm/tests: hdmi: Add Fixed/Constrained rate " Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 17:57 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 24/27] drm/tests: hdmi: Add Quick Media Switching tests Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 25/27] drm/connector: Fix docs for cnmvrr and cinemavrr members Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 15:52 ` [PATCH v2 26/27] drm/debugfs: Expose HDMI VRR in vrr_range debugfs Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 18:05 ` sashiko-bot
2026-11-05 15:52 ` [PATCH v2 27/27] drm/tests: hdmi: Add tests for vrr_display_{min,max} properties Nicolas Frattaroli
2026-11-05 15:52 ` Nicolas Frattaroli
2026-11-05 18:03 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.