[PATCH 1/2] drm/amd/display: Saturate refresh rates in v1/v2 FreeSync infopackets
From: Cooper Morgan
Date: Sat Oct 31 2026 - 00:19:26 EST
The v1 and v2 FreeSync infopackets carry the minimum and maximum
refresh rate in one byte each, and build_vrr_infopacket_data_v1() casts
the rate in Hz to unsigned char. On a sink whose range goes above
255 Hz the value wraps: a 320 Hz mode is advertised as a 64 Hz maximum.
Sinks that treat the field as authoritative misbehave once the wrapped
value lands inside their range. An MSI MAG 271QPX32 (48-320 Hz) over
native DP looks like a ~720p image upscaled to the full screen at 320 Hz
and runs its self-test pattern at 305-315 Hz, while 300 Hz (wrapping to
44 Hz, below its minimum) is fine. Capping the EDID range at 255 Hz, so
the packet says 255, gives a correct picture at 320 Hz.
Saturate the fields at 255 Hz instead of wrapping.
Capping v1 at 255 as a fallback was suggested by oystersuki in the linked
issue.
Fixes: 98e6436d3af5 ("drm/amd/display: Refactor FreeSync module")
Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/5988
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Cooper Morgan <ext-github@xxxxxxxxxxx>
---
.../amdgpu_dm/tests/amdgpu_dm_freesync_test.c | 57 +++++++++++++++++++
.../amd/display/modules/freesync/freesync.c | 12 +++-
2 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_freesync_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_freesync_test.c
index 710cdc7f4..b603516e2 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_freesync_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_freesync_test.c
@@ -664,6 +664,62 @@ static void dm_test_update_freesync_state_pcon_default(struct kunit *test)
dm_test_update_freesync_state_pcon_version(test, 0);
}
+/*
+ * Build the VRR infopacket for a non-PCON, non-Replay sink with VRR inactive
+ * over 48Hz to @mode_hz, the max that mod_freesync_build_vrr_params() clamps
+ * to the mode's rate.
+ */
+static struct dc_info_packet
+dm_test_freesync_packet(struct kunit *test, enum signal_type signal,
+ u8 vsdb_version, int sink_max_hz, int mode_hz)
+{
+ struct amdgpu_device *adev = dm_kunit_alloc_adev(test);
+ struct dc_link *link = dm_kunit_alloc_link(test);
+ struct dc_stream_state *stream = dm_kunit_alloc_stream(test, link);
+ struct amdgpu_dm_connector *aconnector;
+ struct mod_vrr_params *vrr;
+ struct amdgpu_crtc *acrtc;
+ struct dm_crtc_state crtc_state = { 0 };
+
+ acrtc = kunit_kzalloc(test, sizeof(*acrtc), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, acrtc);
+ aconnector = kunit_kzalloc(test, sizeof(*aconnector), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, aconnector);
+ crtc_state.base.crtc = &acrtc->base;
+ stream->timing.h_total = 2200;
+ stream->timing.v_total = 1125;
+ stream->signal = signal;
+ stream->dm_stream_context = aconnector;
+ aconnector->vsdb_info.amd_vsdb_version = vsdb_version;
+ aconnector->max_vfreq = sink_max_hz;
+ vrr = &acrtc->dm_irq_params.vrr_params;
+ vrr->send_info_frame = true;
+ vrr->state = VRR_STATE_INACTIVE;
+ vrr->min_refresh_in_uhz = 48000000;
+ vrr->max_refresh_in_uhz = mode_hz * 1000000;
+ adev->dm.adev = adev;
+
+ amdgpu_dm_update_freesync_state_on_stream(&adev->dm, &crtc_state, stream, NULL, 0);
+
+ return crtc_state.vrr_infopacket;
+}
+
+/**
+ * dm_test_update_freesync_state_dp_v2_saturates - Test v1 saturates above 255Hz
+ * @test: The KUnit test context
+ *
+ * A VSDB v2 sink keeps the v1 packet, which cannot carry 320Hz.
+ */
+static void dm_test_update_freesync_state_dp_v2_saturates(struct kunit *test)
+{
+ struct dc_info_packet p =
+ dm_test_freesync_packet(test, SIGNAL_TYPE_DISPLAY_PORT, 2, 320, 320);
+
+ KUNIT_EXPECT_EQ(test, p.hb3, 0x04);
+ KUNIT_EXPECT_EQ(test, p.sb[7], 255);
+ KUNIT_EXPECT_EQ(test, p.sb[8], 255);
+}
+
/* Tests for amdgpu_dm_update_stream_irq_parameters() */
/**
@@ -900,6 +956,7 @@ static struct kunit_case amdgpu_dm_freesync_tests[] = {
KUNIT_CASE(dm_test_update_freesync_state_pcon_v2),
KUNIT_CASE(dm_test_update_freesync_state_pcon_v3),
KUNIT_CASE(dm_test_update_freesync_state_pcon_default),
+ KUNIT_CASE(dm_test_update_freesync_state_dp_v2_saturates),
/* amdgpu_dm_update_stream_irq_parameters */
KUNIT_CASE(dm_test_update_stream_irq_parameters_no_stream),
KUNIT_CASE(dm_test_update_stream_irq_parameters_invalid_timing),
diff --git a/drivers/gpu/drm/amd/display/modules/freesync/freesync.c b/drivers/gpu/drm/amd/display/modules/freesync/freesync.c
index 4bd060e30..af7054668 100644
--- a/drivers/gpu/drm/amd/display/modules/freesync/freesync.c
+++ b/drivers/gpu/drm/amd/display/modules/freesync/freesync.c
@@ -571,6 +571,12 @@ static bool vrr_settings_require_update(struct core_freesync *core_freesync,
return false;
}
+/* v1/v2 packets hold each rate in one byte; saturate instead of wrapping. */
+static unsigned char vrr_refresh_hz_u8(unsigned int refresh_in_uhz)
+{
+ return min((refresh_in_uhz + 500000) / 1000000, 255U);
+}
+
static void build_vrr_infopacket_data_v1(const struct mod_vrr_params *vrr,
struct dc_info_packet *infopacket,
bool freesync_on_desktop)
@@ -614,15 +620,15 @@ static void build_vrr_infopacket_data_v1(const struct mod_vrr_params *vrr,
/* PB7 = FreeSync Minimum refresh rate (Hz) */
if (vrr->state == VRR_STATE_ACTIVE_VARIABLE ||
vrr->state == VRR_STATE_ACTIVE_FIXED) {
- infopacket->sb[7] = (unsigned char)((vrr->min_refresh_in_uhz + 500000) / 1000000);
+ infopacket->sb[7] = vrr_refresh_hz_u8(vrr->min_refresh_in_uhz);
} else {
- infopacket->sb[7] = (unsigned char)((vrr->max_refresh_in_uhz + 500000) / 1000000);
+ infopacket->sb[7] = vrr_refresh_hz_u8(vrr->max_refresh_in_uhz);
}
/* PB8 = FreeSync Maximum refresh rate (Hz)
* Note: We should never go above the field rate of the mode timing set.
*/
- infopacket->sb[8] = (unsigned char)((vrr->max_refresh_in_uhz + 500000) / 1000000);
+ infopacket->sb[8] = vrr_refresh_hz_u8(vrr->max_refresh_in_uhz);
}
static void build_vrr_infopacket_data_v3(const struct mod_vrr_params *vrr,
--
2.56.0