mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] drm/amd/display: Keep eDP native timing after resume with video=
@ 2026-10-14 22:30 Hugo Sales
  2026-10-14 22:30 ` [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
  2026-10-14 22:30 ` [PATCH " Hugo Sales
  0 siblings, 2 replies; 9+ messages in thread
From: Hugo Sales @ 2026-10-14 22:30 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Hugo Sales

On a 3840x2400 eDP panel booted with video=eDP-1:1920x1200@60, an HDMI
hotplug followed by suspend/resume brings the panel back with the
1920x1200 mode as its raw timing. The image ends up squeezed into the
top-left corner. Photos, hardware details and the test loop are in
https://gitlab.freedesktop.org/drm/amd/-/issues/5983

The hotplug makes fbdev emulation reprobe every connector with its
framebuffer size as the limit, so connector->modes loses the 3840x2400
mode. On resume, create_stream_for_sink() finds no preferred mode in the
list and scales onto the first mode instead. Patch 1 falls back to the
encoder's native_mode, which amdgpu_dm_get_native_mode() copies from the
preferred mode before the size limits apply. Patch 2 adds KUnit tests
for it

The fallback only applies to eDP and LVDS, where amdgpu turns scaling on
by itself. On other connectors the preferred mode can also be missing
because mode_valid rejected it, and then scaling onto the first valid
mode is right. Those connectors still hit this bug when userspace sets
the scaling mode property, which this series doesn't fix

A fuller amdgpu fix would always take the eDP and LVDS native timing
from the encoder, and search connector->modes only when the encoder has
none. I kept this to a fallback so it changes less, but I can respin it
that way if you'd prefer

On amd-staging-drm-next, with an HDMI hotplug and suspend/resume
(pm_test=platform) in a loop, 28 of 29 resumes came back with the wrong
timing without the patch, and the other showed it briefly. With it, 169
of 169 were fine. A backport to 7.2.8 went from 103 of 103 bad to 1363
of 1363 good. The new KUnit tests fail without the fix, and the
connector suite passes, apart from dm_test_fs_caps_hdmi_vsdb and
dm_test_fs_caps_pcon_whitelist, which already fail on this tree

The cause could also be fixed in DRM core, for every connector, by
making drm_client_modeset_probe() probe with the device's limits and
apply the framebuffer size only when fbdev picks its own mode. That
touches more shared code, since fbdev's other mode pickers would then
need size checks too

Hugo Sales (2):
  drm/amd/display: Fall back to the native mode, not the first, on
    eDP/LVDS
  drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback

 .../display/amdgpu_dm/amdgpu_dm_connector.c   |  28 +++++
 .../tests/amdgpu_dm_connector_test.c          | 112 ++++++++++++++++++
 2 files changed, 140 insertions(+)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS
  2026-10-14 22:30 [PATCH 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
@ 2026-10-14 22:30 ` Hugo Sales
  2026-10-23 18:26   ` Chung, ChiaHsuan (Tom)
  2026-10-14 22:30 ` [PATCH " Hugo Sales
  1 sibling, 1 reply; 9+ messages in thread
From: Hugo Sales @ 2026-10-14 22:30 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Hugo Sales

Currently, create_stream_for_sink() takes the native timing for a scaled
mode from the preferred mode in connector->modes. After fbdev emulation
probes with size limits below the native resolution, as it does when
video= sets a smaller mode, that mode is no longer in the list

If that probe is the last one before a suspend, for example after an
HDMI hotplug, the resume finds no preferred mode, and the panel is
driven with the scaled mode's timing instead of its own

amdgpu_dm_get_native_mode() copies the preferred mode to the encoder on
every probe, before the size limits apply. Fall back to that copy when
connector->modes has no preferred mode. Limit this to eDP and LVDS,
where amdgpu turns scaling on by itself. On other connectors mode_valid
can also leave the preferred mode out, and then the first valid mode is
the right fallback

Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/5983
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
 .../display/amdgpu_dm/amdgpu_dm_connector.c   | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
index 428a5f937..a1dc3e287 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
@@ -257,6 +257,8 @@ STATIC_IFN_KUNIT void update_subconnector_property(struct amdgpu_dm_connector *a
 EXPORT_IF_KUNIT(update_subconnector_property);
 
 STATIC_IFN_KUNIT int amdgpu_dm_connector_get_modes(struct drm_connector *connector);
+STATIC_IFN_KUNIT struct drm_encoder *amdgpu_dm_connector_to_encoder(
+	struct drm_connector *connector);
 
 STATIC_IFN_KUNIT void amdgpu_dm_fbc_init(struct drm_connector *connector)
 {
@@ -1470,6 +1472,7 @@ create_stream_for_sink(struct drm_connector *connector,
 	struct dc_stream_state *stream = NULL;
 	struct drm_display_mode mode;
 	struct drm_display_mode saved_mode;
+	struct drm_display_mode native_mode;
 	struct drm_display_mode *freesync_mode = NULL;
 	bool native_mode_found = false;
 	bool recalculate_timing = false;
@@ -1529,6 +1532,31 @@ create_stream_for_sink(struct drm_connector *connector,
 			break;
 		}
 	}
+
+	/*
+	 * After a probe with size limits smaller than the preferred mode,
+	 * connector->modes no longer has that mode, and the first mode left
+	 * would become the stream timing. fbdev emulation probes with limits
+	 * that video= can set below the native size. The encoder has a copy of
+	 * the preferred mode from amdgpu_dm_get_native_mode(), made on every
+	 * probe before the limits apply, so use that copy. Only do this for eDP
+	 * and LVDS panels, which take only their native timing. On other
+	 * connectors mode_valid can also reject the preferred mode, and then
+	 * the first valid mode is the right one
+	 */
+	if (!native_mode_found && aconnector && !aconnector->mst_root && aconnector->drm_edid &&
+	    (connector->connector_type == DRM_MODE_CONNECTOR_eDP ||
+	     connector->connector_type == DRM_MODE_CONNECTOR_LVDS)) {
+		struct drm_encoder *encoder = amdgpu_dm_connector_to_encoder(connector);
+
+		if (encoder && to_amdgpu_encoder(encoder)->native_mode.clock) {
+			drm_mode_copy(&native_mode, &to_amdgpu_encoder(encoder)->native_mode);
+			drm_mode_set_crtcinfo(&native_mode, CRTC_INTERLACE_HALVE_V);
+			preferred_mode = &native_mode;
+			native_mode_found = true;
+		}
+	}
+
 	if (!native_mode_found)
 		preferred_mode = list_first_entry_or_null(
 				&connector->modes,
-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback
  2026-10-14 22:30 [PATCH 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
  2026-10-14 22:30 ` [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
@ 2026-10-14 22:30 ` Hugo Sales
  1 sibling, 0 replies; 9+ messages in thread
From: Hugo Sales @ 2026-10-14 22:30 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Hugo Sales

Leave only a scaled mode in connector->modes and put the larger native
mode in the encoder, as a size-limited probe does. On eDP and LVDS,
create_stream_for_sink() must build a stream with the native timing and
the scaled mode as its source size. On HDMI it must keep the first mode
in the list as the timing

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
 .../tests/amdgpu_dm_connector_test.c          | 112 ++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
index 56fd133bc..55e2c3849 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
@@ -4547,6 +4547,115 @@ static void dm_test_create_stream_hdmi_vsif(struct kunit *test)
 	dc_sink_release(sink);
 }
 
+/*
+ * Build a stream after a probe with size limits below the native mode, such as
+ * fbdev emulation's. That probe leaves only a 1920x1200 scaled mode in the
+ * connector's mode list, while the encoder keeps the 3840x2400 native mode
+ */
+static struct dc_stream_state *
+dm_test_native_mode_pruned_stream(struct kunit *test, int connector_type)
+{
+	static const u8 edid[EDID_LENGTH];
+	struct dm_test_stream_ctx *ctx = dm_test_stream_ctx_alloc(test);
+	struct amdgpu_encoder *aenc;
+	struct dc_stream_state *stream;
+	struct drm_display_mode *native;
+
+	ctx->aconnector->base.connector_type = connector_type;
+
+	aenc = drmm_kzalloc(ctx->drm, sizeof(*aenc), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, aenc);
+	KUNIT_ASSERT_EQ(test, drmm_encoder_init(ctx->drm, &aenc->base, NULL,
+						DRM_MODE_ENCODER_TMDS, NULL), 0);
+	KUNIT_ASSERT_EQ(test, drm_connector_attach_encoder(&ctx->aconnector->base, &aenc->base), 0);
+
+	native = &aenc->native_mode;
+	native->clock = 592500;
+	native->hdisplay = 3840;
+	native->hsync_start = 3888;
+	native->hsync_end = 3920;
+	native->htotal = 4000;
+	native->vdisplay = 2400;
+	native->vsync_start = 2403;
+	native->vsync_end = 2409;
+	native->vtotal = 2469;
+	/* Cached by the first, unlimited probe */
+	drm_mode_copy(&ctx->aconnector->freesync_vid_base, native);
+
+	drm_mode_copy(ctx->mode, native);
+	ctx->mode->hdisplay = 1920;
+	ctx->mode->vdisplay = 1200;
+	drm_mode_set_crtcinfo(ctx->mode, CRTC_INTERLACE_HALVE_V);
+	dm_test_stream_add_mode(test, ctx, 0);
+	ctx->dm_state->scaling = RMX_FULL;
+
+	ctx->aconnector->drm_edid = drm_edid_alloc(edid, sizeof(edid));
+	KUNIT_ASSERT_NOT_NULL(test, ctx->aconnector->drm_edid);
+
+	stream = create_stream_for_sink(&ctx->aconnector->base, ctx->mode,
+					ctx->dm_state, NULL, 8,
+					PIXEL_ENCODING_RGB, false);
+
+	drm_edid_free(ctx->aconnector->drm_edid);
+	KUNIT_ASSERT_NOT_NULL(test, stream);
+
+	return stream;
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_edp - Test eDP gets native timing
+ * @test: The KUnit test context
+ *
+ * The scaled mode must still be driven with the panel's native timing, kept in
+ * the encoder
+ */
+static void dm_test_create_stream_native_mode_pruned_edp(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_pruned_stream(test, DRM_MODE_CONNECTOR_eDP);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_lvds - Test LVDS gets native timing
+ * @test: The KUnit test context
+ */
+static void dm_test_create_stream_native_mode_pruned_lvds(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_pruned_stream(test, DRM_MODE_CONNECTOR_LVDS);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_hdmi - Test HDMI uses the first mode
+ * @test: The KUnit test context
+ *
+ * On a connector that is not a panel, mode_valid may have dropped the native
+ * mode, so the first mode left in the list must drive the timing
+ */
+static void dm_test_create_stream_native_mode_pruned_hdmi(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_pruned_stream(test, DRM_MODE_CONNECTOR_HDMIA);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 1200);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
 /* Tests for amdgpu_dm_connector_detect() */
 
 /*
@@ -10246,6 +10355,9 @@ static struct kunit_case amdgpu_dm_connector_tests[] = {
 	KUNIT_CASE(dm_test_create_stream_dp_vsc_colorimetry),
 	KUNIT_CASE(dm_test_create_stream_dp_colorimetry_quirk),
 	KUNIT_CASE(dm_test_create_stream_hdmi_vsif),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_edp),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_lvds),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_hdmi),
 	/* amdgpu_dm_connector_detect */
 	KUNIT_CASE(dm_test_detect_force_on),
 	KUNIT_CASE(dm_test_detect_force_on_digital),
-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS
  2026-10-14 22:30 ` [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
@ 2026-10-23 18:26   ` Chung, ChiaHsuan (Tom)
  2026-10-24 16:33     ` [PATCH v2 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
  0 siblings, 1 reply; 9+ messages in thread
From: Chung, ChiaHsuan (Tom) @ 2026-10-23 18:26 UTC (permalink / raw)
  To: Hugo Sales, amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher


On 10/15/2026 6:30 AM, Hugo Sales wrote:
> Currently, create_stream_for_sink() takes the native timing for a scaled
> mode from the preferred mode in connector->modes. After fbdev emulation
> probes with size limits below the native resolution, as it does when
> video= sets a smaller mode, that mode is no longer in the list
>
> If that probe is the last one before a suspend, for example after an
> HDMI hotplug, the resume finds no preferred mode, and the panel is
> driven with the scaled mode's timing instead of its own
>
> amdgpu_dm_get_native_mode() copies the preferred mode to the encoder on
> every probe, before the size limits apply. Fall back to that copy when
> connector->modes has no preferred mode. Limit this to eDP and LVDS,
> where amdgpu turns scaling on by itself. On other connectors mode_valid
> can also leave the preferred mode out, and then the first valid mode is
> the right fallback
>
> Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/5983
> Assisted-by: Claude:claude-opus-5-5
> Signed-off-by: Hugo Sales <hugo@hsal.es>

Hi Hugo,

Thanks for the patch and the detailed analysis.
This no longer applies to the current amd-staging-drm-next. It
conflicts with commit 3299935e65c0 ("drm/amd/display: Use separate
iterator for preferred mode lookup"), which landed after the tree you
based this on. That commit removed native_mode_found from
create_stream_for_sink() and now checks preferred_mode directly:

     if (!preferred_mode)
         preferred_mode = list_first_entry_or_null(...);

Could you rebase the series onto the latest amd-staging-drm-next and
send a v2? The change should be mechanical: test !preferred_mode
instead of !native_mode_found, and drop the native_mode_found = true
assignment. The logic stays the same. Patch 2/2 still applies cleanly.
Also, the dm_test_fs_caps_hdmi_vsdb and dm_test_fs_caps_pcon_whitelist
failures you mentioned in the cover letter were fixed by commit
5ac0b4dd2a1f ("drm/amd/display: Fix FreeSync caps tests after common
EDID VSDB switch"). With the series rebased that way, the whole
amdgpu_dm_connector KUnit suite passes on our side, including your new
tests.


Thanks,
Tom

> ---
>   .../display/amdgpu_dm/amdgpu_dm_connector.c   | 28 +++++++++++++++++++
>   1 file changed, 28 insertions(+)
>
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> index 428a5f937..a1dc3e287 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> @@ -257,6 +257,8 @@ STATIC_IFN_KUNIT void update_subconnector_property(struct amdgpu_dm_connector *a
>   EXPORT_IF_KUNIT(update_subconnector_property);
>
>   STATIC_IFN_KUNIT int amdgpu_dm_connector_get_modes(struct drm_connector *connector);
> +STATIC_IFN_KUNIT struct drm_encoder *amdgpu_dm_connector_to_encoder(
> +       struct drm_connector *connector);
>
>   STATIC_IFN_KUNIT void amdgpu_dm_fbc_init(struct drm_connector *connector)
>   {
> @@ -1470,6 +1472,7 @@ create_stream_for_sink(struct drm_connector *connector,
>          struct dc_stream_state *stream = NULL;
>          struct drm_display_mode mode;
>          struct drm_display_mode saved_mode;
> +       struct drm_display_mode native_mode;
>          struct drm_display_mode *freesync_mode = NULL;
>          bool native_mode_found = false;
>          bool recalculate_timing = false;
> @@ -1529,6 +1532,31 @@ create_stream_for_sink(struct drm_connector *connector,
>                          break;
>                  }
>          }
> +
> +       /*
> +        * After a probe with size limits smaller than the preferred mode,
> +        * connector->modes no longer has that mode, and the first mode left
> +        * would become the stream timing. fbdev emulation probes with limits
> +        * that video= can set below the native size. The encoder has a copy of
> +        * the preferred mode from amdgpu_dm_get_native_mode(), made on every
> +        * probe before the limits apply, so use that copy. Only do this for eDP
> +        * and LVDS panels, which take only their native timing. On other
> +        * connectors mode_valid can also reject the preferred mode, and then
> +        * the first valid mode is the right one
> +        */
> +       if (!native_mode_found && aconnector && !aconnector->mst_root && aconnector->drm_edid &&
> +           (connector->connector_type == DRM_MODE_CONNECTOR_eDP ||
> +            connector->connector_type == DRM_MODE_CONNECTOR_LVDS)) {
> +               struct drm_encoder *encoder = amdgpu_dm_connector_to_encoder(connector);
> +
> +               if (encoder && to_amdgpu_encoder(encoder)->native_mode.clock) {
> +                       drm_mode_copy(&native_mode, &to_amdgpu_encoder(encoder)->native_mode);
> +                       drm_mode_set_crtcinfo(&native_mode, CRTC_INTERLACE_HALVE_V);
> +                       preferred_mode = &native_mode;
> +                       native_mode_found = true;
> +               }
> +       }
> +
>          if (!native_mode_found)
>                  preferred_mode = list_first_entry_or_null(
>                                  &connector->modes,
> --
> 2.55.0
>

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 0/2] drm/amd/display: Keep eDP native timing after resume with video=
  2026-10-23 18:26   ` Chung, ChiaHsuan (Tom)
@ 2026-10-24 16:33     ` Hugo Sales
  2026-10-24 16:33       ` [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
  2026-10-24 16:33       ` [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback Hugo Sales
  0 siblings, 2 replies; 9+ messages in thread
From: Hugo Sales @ 2026-10-24 16:33 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Chung, ChiaHsuan (Tom)

On a 3840x2400 eDP panel booted with video=eDP-1:1920x1200@60, an HDMI
hotplug followed by suspend/resume brings the panel back with the
1920x1200 mode as its raw timing. The image ends up squeezed into the
top-left corner. Photos, hardware details and the test loop are in
https://gitlab.freedesktop.org/drm/amd/-/issues/5983

The hotplug makes fbdev emulation reprobe every connector with its
framebuffer size as the limit, so connector->modes loses the 3840x2400
mode. On resume, create_stream_for_sink() finds no preferred mode in the
list and scales onto the first mode instead. Patch 1 falls back to a
mode of the native size if the list still has one, and otherwise to the
encoder's native_mode, which amdgpu_dm_get_native_mode() copies from the
preferred mode before the size limits apply. Patch 2 adds KUnit tests
for it

The fallback only applies to eDP and LVDS, where amdgpu turns scaling on
by itself. On other connectors the preferred mode can also be missing
because mode_valid rejected it, and then scaling onto the first valid
mode is right. From reading the code, other connectors take the same
path when userspace sets the scaling mode property after fbdev pruned
the list, but I haven't tried that, and this series doesn't change it

A fuller amdgpu fix would always drive eDP and LVDS at their native
size, not only when the preferred mode is missing. I kept this to a
fallback so it changes less, but I can respin it that way if you'd
prefer

I left out one other case, also because the fix is big. On eDP the probe
can drop every mode of the native size by itself, for example when
mode_valid rejects the native timing after link training settles on a
lower rate. The list then has no native size mode, and the fallback
takes the encoder's copy, a timing DC may reject, where before it took
the first mode. I have a follow-up that uses the copy only when the last
probe with room for the native size kept that exact timing, and retries
without it if DC rejects it. It's about 170 lines plus tests, for a case
I haven't hit on hardware. Sashiko, running locally, has no findings on
it, but the instance on the list may see it differently. It replaces
both patches here and is at
https://github.com/someonewithpc/linux/tree/edp-native-mode-validated
I can send it if you think it's worth it

On amd-staging-drm-next before this rebase, with an HDMI hotplug and
suspend/resume (pm_test=platform) in a loop, 28 of 29 resumes came back
with the wrong timing without the patch, and the other showed it
briefly. With it, 169 of 169 were fine. A backport to 7.2.8 went from
103 of 103 bad to 1363 of 1363 good. Those runs predate the lookup of a
native size mode in the list, but in this case the list has none, so
the patch takes the same path. On the current base the
amdgpu_dm_connector KUnit suite passes, 358 of 358. Without the fix, the
new eDP and LVDS tests fail and the HDMI one passes. Without the lookup,
the test with a native size mode in the list fails

I've also sent a DRM core series that fixes the cause for every
driver. It makes drm_client_modeset_probe() probe with the device's
limits and has fbdev's mode pickers check the framebuffer size
themselves. Either series alone fixes the reported bug:
https://lore.kernel.org/all/20261015212545.1989962-1-hugo@hsal.es/

v1: https://lore.kernel.org/r/20261014223041.171317-1-hugo@hsal.es

Changes in v2:
- Use drm_mode_init() for the on-stack copy of the encoder's native
  mode. drm_mode_copy() reads the destination's list head, which was
  uninitialized (Sashiko)
- If connector->modes still has a mode of the native size, use it
  instead of the encoder's copy, whose timing mode_valid may have
  rejected (Sashiko)
- Build the copy in saved_mode, which this path doesn't use, so the
  stack frame doesn't grow (Sashiko)
- KUnit cases for each condition of the fallback
- Link the DRM core series, now sent
- Rebased onto amd-staging-drm-next at 77f548b127c6, as Tom Chung
  asked. That includes 3299935e65c0 ("drm/amd/display: Use separate
  iterator for preferred mode lookup"), which changes the same lookup
  but keeps the fall back to the first mode, so this fix still applies.
  It now checks for a NULL preferred_mode, since native_mode_found is
  gone

Hugo Sales (2):
  drm/amd/display: Fall back to the native mode, not the first, on
    eDP/LVDS
  drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback

 .../display/amdgpu_dm/amdgpu_dm_connector.c   |  47 ++++
 .../tests/amdgpu_dm_connector_test.c          | 226 +++++++++++++++++-
 2 files changed, 271 insertions(+), 2 deletions(-)


base-commit: 77f548b127c6de34ce271fa09ba270cb1ef9f2da
-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS
  2026-10-24 16:33     ` [PATCH v2 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
@ 2026-10-24 16:33       ` Hugo Sales
  2026-10-31  2:17         ` Alex Hung
  2026-10-24 16:33       ` [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback Hugo Sales
  1 sibling, 1 reply; 9+ messages in thread
From: Hugo Sales @ 2026-10-24 16:33 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Chung, ChiaHsuan (Tom)

Currently, create_stream_for_sink() takes the native timing for a scaled
mode from the preferred mode in connector->modes. After fbdev emulation
probes with size limits below the native resolution, as it does when
video= sets a smaller mode, that mode is no longer in the list

If that probe is the last one before a suspend, for example after an
HDMI hotplug, the resume finds no preferred mode, and the panel is
driven with the scaled mode's timing instead of its own

amdgpu_dm_get_native_mode() copies the preferred mode to the encoder on
every probe, before the size limits apply. When connector->modes has no
preferred mode, take a mode of the native size from the list, or that
copy if the limits pruned them all. The list comes first because
mode_valid may have rejected the copy's timing and kept another mode of
the same size. Limit this to eDP and LVDS, where amdgpu turns scaling on
by itself. On other connectors mode_valid can also leave the preferred
mode out, and then the first valid mode is the right fallback

Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/5983
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
 .../display/amdgpu_dm/amdgpu_dm_connector.c   | 47 +++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
index ded8e21897f1c..a13e3b641fd27 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
@@ -257,6 +257,8 @@ STATIC_IFN_KUNIT void update_subconnector_property(struct amdgpu_dm_connector *a
 EXPORT_IF_KUNIT(update_subconnector_property);
 
 STATIC_IFN_KUNIT int amdgpu_dm_connector_get_modes(struct drm_connector *connector);
+STATIC_IFN_KUNIT struct drm_encoder *amdgpu_dm_connector_to_encoder(
+	struct drm_connector *connector);
 
 STATIC_IFN_KUNIT void amdgpu_dm_fbc_init(struct drm_connector *connector)
 {
@@ -1545,6 +1547,51 @@ create_stream_for_sink(struct drm_connector *connector,
 			break;
 		}
 	}
+
+	/*
+	 * After a probe with size limits smaller than the preferred mode,
+	 * connector->modes no longer has that mode, and the first mode left
+	 * would become the stream timing. fbdev emulation probes with limits
+	 * that video= can set below the native size. Only eDP and LVDS panels
+	 * get a fallback here, since they take only their native size. On other
+	 * connectors mode_valid can also reject the preferred mode, and then
+	 * the first valid mode is the right one
+	 *
+	 * Take a mode of the native size from connector->modes if one is left,
+	 * since mode_valid may have rejected the preferred timing itself. The
+	 * probe sorts the list, so the first one has the highest refresh rate
+	 * left. If the limits pruned them all, use the copy of the preferred
+	 * mode that amdgpu_dm_get_native_mode() keeps in the encoder. It is
+	 * made on every probe, before the limits apply
+	 */
+	if (!preferred_mode && aconnector && aconnector->drm_edid &&
+	    (connector->connector_type == DRM_MODE_CONNECTOR_eDP ||
+	     connector->connector_type == DRM_MODE_CONNECTOR_LVDS)) {
+		struct drm_encoder *encoder = amdgpu_dm_connector_to_encoder(connector);
+		const struct drm_display_mode *enc_native =
+			encoder ? &to_amdgpu_encoder(encoder)->native_mode : NULL;
+
+		if (enc_native && enc_native->clock) {
+			list_for_each_entry(connector_mode, &connector->modes, head) {
+				if (connector_mode->hdisplay == enc_native->hdisplay &&
+				    connector_mode->vdisplay == enc_native->vdisplay) {
+					preferred_mode = connector_mode;
+					break;
+				}
+			}
+
+			/*
+			 * saved_mode is only used for FreeSync video modes, and
+			 * that path doesn't read preferred_mode
+			 */
+			if (!preferred_mode) {
+				drm_mode_init(&saved_mode, enc_native);
+				drm_mode_set_crtcinfo(&saved_mode, CRTC_INTERLACE_HALVE_V);
+				preferred_mode = &saved_mode;
+			}
+		}
+	}
+
 	if (!preferred_mode)
 		preferred_mode = list_first_entry_or_null(
 				&connector->modes,
-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback
  2026-10-24 16:33     ` [PATCH v2 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
  2026-10-24 16:33       ` [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
@ 2026-10-24 16:33       ` Hugo Sales
  2026-10-31  2:17         ` Alex Hung
  1 sibling, 1 reply; 9+ messages in thread
From: Hugo Sales @ 2026-10-24 16:33 UTC (permalink / raw)
  To: amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Chung, ChiaHsuan (Tom)

Leave only a scaled mode in connector->modes and put the larger native
mode in the encoder, as a size-limited probe does. On eDP and LVDS,
create_stream_for_sink() must build a stream with the native timing and
the scaled mode as its source size. On HDMI it must keep the first mode
in the list as the timing

When the list also has a mode of the native size with a different
timing, eDP must use that mode and not the encoder's copy

The fallback must stay off when the list has a preferred mode, without
an EDID, and when the encoder's copy has no clock

Signed-off-by: Hugo Sales <hugo@hsal.es>
---
 .../tests/amdgpu_dm_connector_test.c          | 226 +++++++++++++++++-
 1 file changed, 224 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
index d291ae40ca1d0..af71ca660bfea 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
@@ -4181,7 +4181,8 @@ struct dm_test_stream_ctx {
 	struct drm_display_mode *mode;
 };
 
-static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
+static struct dm_test_stream_ctx *
+dm_test_stream_ctx_alloc_type(struct kunit *test, int connector_type)
 {
 	struct dm_test_stream_ctx *ctx;
 	struct device *dev;
@@ -4202,7 +4203,7 @@ static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
 	KUNIT_ASSERT_EQ(test,
 		drmm_connector_init(ctx->drm, &ctx->aconnector->base,
 				    &dm_test_connector_funcs,
-				    DRM_MODE_CONNECTOR_DisplayPort, NULL), 0);
+				    connector_type, NULL), 0);
 
 	ctx->dc_ctx = kunit_kzalloc(test, sizeof(*ctx->dc_ctx), GFP_KERNEL);
 	KUNIT_ASSERT_NOT_NULL(test, ctx->dc_ctx);
@@ -4229,6 +4230,11 @@ static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
 	return ctx;
 }
 
+static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
+{
+	return dm_test_stream_ctx_alloc_type(test, DRM_MODE_CONNECTOR_DisplayPort);
+}
+
 /**
  * dm_test_create_stream_fake_sink_success - Test a stream is built from a fake sink
  * @test: The KUnit test context
@@ -4547,6 +4553,215 @@ static void dm_test_create_stream_hdmi_vsif(struct kunit *test)
 	dc_sink_release(sink);
 }
 
+enum dm_test_native {
+	/*
+	 * The list also keeps a 3840x2400 mode at half the native clock, as if
+	 * mode_valid had rejected the native timing and no limit applied
+	 */
+	DM_TEST_NATIVE_LISTED = BIT(0),
+	/* The scaled mode is the preferred one */
+	DM_TEST_NATIVE_PREFERRED = BIT(1),
+	DM_TEST_NATIVE_NO_CLOCK = BIT(2),
+	DM_TEST_NATIVE_NO_EDID = BIT(3),
+};
+
+/*
+ * Build a stream after a probe with size limits below the native mode, such as
+ * fbdev emulation's. That probe leaves only a 1920x1200 scaled mode in the
+ * connector's mode list, while the encoder keeps the 3840x2400 native mode.
+ * @flags is a mask of enum dm_test_native
+ */
+static struct dc_stream_state *
+dm_test_native_mode_stream(struct kunit *test, int connector_type, unsigned int flags)
+{
+	static const u8 edid[EDID_LENGTH];
+	struct dm_test_stream_ctx *ctx = dm_test_stream_ctx_alloc_type(test, connector_type);
+	struct amdgpu_encoder *aenc;
+	struct dc_stream_state *stream;
+	struct drm_display_mode *native;
+
+	aenc = drmm_kzalloc(ctx->drm, sizeof(*aenc), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, aenc);
+	KUNIT_ASSERT_EQ(test, drmm_encoder_init(ctx->drm, &aenc->base, NULL,
+						DRM_MODE_ENCODER_TMDS, NULL), 0);
+	KUNIT_ASSERT_EQ(test, drm_connector_attach_encoder(&ctx->aconnector->base, &aenc->base), 0);
+
+	native = &aenc->native_mode;
+	native->clock = 592500;
+	native->hdisplay = 3840;
+	native->hsync_start = 3888;
+	native->hsync_end = 3920;
+	native->htotal = 4000;
+	native->vdisplay = 2400;
+	native->vsync_start = 2403;
+	native->vsync_end = 2409;
+	native->vtotal = 2469;
+	/* Cached by the first, unlimited probe */
+	drm_mode_copy(&ctx->aconnector->freesync_vid_base, native);
+
+	drm_mode_copy(ctx->mode, native);
+	ctx->mode->hdisplay = 1920;
+	ctx->mode->vdisplay = 1200;
+	drm_mode_set_crtcinfo(ctx->mode, CRTC_INTERLACE_HALVE_V);
+	dm_test_stream_add_mode(test, ctx, flags & DM_TEST_NATIVE_PREFERRED ?
+				DRM_MODE_TYPE_PREFERRED : 0);
+
+	/* After the scaled mode, so that taking the first mode can't pass */
+	if (flags & DM_TEST_NATIVE_LISTED) {
+		struct drm_display_mode *listed = drm_mode_duplicate(ctx->drm, ctx->mode);
+
+		KUNIT_ASSERT_NOT_NULL(test, listed);
+		listed->hdisplay = native->hdisplay;
+		listed->vdisplay = native->vdisplay;
+		listed->clock /= 2;
+		drm_mode_set_crtcinfo(listed, CRTC_INTERLACE_HALVE_V);
+		list_add_tail(&listed->head, &ctx->aconnector->base.modes);
+	}
+	ctx->dm_state->scaling = RMX_FULL;
+
+	if (flags & DM_TEST_NATIVE_NO_CLOCK)
+		native->clock = 0;
+	if (!(flags & DM_TEST_NATIVE_NO_EDID)) {
+		ctx->aconnector->drm_edid = drm_edid_alloc(edid, sizeof(edid));
+		KUNIT_ASSERT_NOT_NULL(test, ctx->aconnector->drm_edid);
+	}
+
+	stream = create_stream_for_sink(&ctx->aconnector->base, ctx->mode,
+					ctx->dm_state, NULL, 8,
+					PIXEL_ENCODING_RGB, false);
+
+	drm_edid_free(ctx->aconnector->drm_edid);
+	ctx->aconnector->drm_edid = NULL;
+	KUNIT_ASSERT_NOT_NULL(test, stream);
+
+	return stream;
+}
+
+/* Expect the stream to be driven with the 1920x1200 mode left in the list */
+static void dm_test_expect_first_mode(struct kunit *test, struct dc_stream_state *stream)
+{
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 1200);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_edp - Test eDP gets native timing
+ * @test: The KUnit test context
+ *
+ * The scaled mode must still be driven with the panel's native timing, kept in
+ * the encoder
+ */
+static void dm_test_create_stream_native_mode_pruned_edp(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP, 0);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 5925000);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_lvds - Test LVDS gets native timing
+ * @test: The KUnit test context
+ */
+static void dm_test_create_stream_native_mode_pruned_lvds(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_LVDS, 0);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 5925000);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_mode_pruned_hdmi - Test HDMI uses the first mode
+ * @test: The KUnit test context
+ *
+ * On a connector that is not a panel, mode_valid may have dropped the native
+ * mode, so the first mode left in the list must drive the timing
+ */
+static void dm_test_create_stream_native_mode_pruned_hdmi(struct kunit *test)
+{
+	dm_test_expect_first_mode(test,
+				  dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_HDMIA, 0));
+}
+
+/**
+ * dm_test_create_stream_native_size_listed_edp - Test eDP prefers a listed mode
+ * @test: The KUnit test context
+ *
+ * When the mode list still has a mode of the native size, that mode's timing
+ * must drive the panel instead of the encoder's copy, which mode_valid may
+ * have rejected
+ */
+static void dm_test_create_stream_native_size_listed_edp(struct kunit *test)
+{
+	struct dc_stream_state *stream =
+		dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
+					   DM_TEST_NATIVE_LISTED);
+
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
+	KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 2962500);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
+	KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
+	dc_stream_release(stream);
+}
+
+/**
+ * dm_test_create_stream_native_preferred_edp - Test a listed preferred mode
+ * wins on eDP
+ * @test: The KUnit test context
+ */
+static void dm_test_create_stream_native_preferred_edp(struct kunit *test)
+{
+	dm_test_expect_first_mode(test,
+				  dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
+							     DM_TEST_NATIVE_LISTED |
+							     DM_TEST_NATIVE_PREFERRED));
+}
+
+/**
+ * dm_test_create_stream_native_mode_no_clock_edp - Test an empty native mode
+ * isn't used
+ * @test: The KUnit test context
+ *
+ * amdgpu_dm_get_native_mode() zeroes the clock and then copies only a
+ * preferred mode at the head of probed_modes. When there is none, the size
+ * left in the encoder is stale and must not pick a listed mode
+ */
+static void dm_test_create_stream_native_mode_no_clock_edp(struct kunit *test)
+{
+	dm_test_expect_first_mode(test,
+				  dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
+							     DM_TEST_NATIVE_LISTED |
+							     DM_TEST_NATIVE_NO_CLOCK));
+}
+
+/**
+ * dm_test_create_stream_native_mode_no_edid_edp - Test the fallback needs an
+ * EDID
+ * @test: The KUnit test context
+ */
+static void dm_test_create_stream_native_mode_no_edid_edp(struct kunit *test)
+{
+	dm_test_expect_first_mode(test,
+				  dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
+							     DM_TEST_NATIVE_LISTED |
+							     DM_TEST_NATIVE_NO_EDID));
+}
+
 /* Tests for amdgpu_dm_connector_detect() */
 
 /*
@@ -10248,6 +10463,13 @@ static struct kunit_case amdgpu_dm_connector_tests[] = {
 	KUNIT_CASE(dm_test_create_stream_dp_vsc_colorimetry),
 	KUNIT_CASE(dm_test_create_stream_dp_colorimetry_quirk),
 	KUNIT_CASE(dm_test_create_stream_hdmi_vsif),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_edp),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_lvds),
+	KUNIT_CASE(dm_test_create_stream_native_mode_pruned_hdmi),
+	KUNIT_CASE(dm_test_create_stream_native_size_listed_edp),
+	KUNIT_CASE(dm_test_create_stream_native_preferred_edp),
+	KUNIT_CASE(dm_test_create_stream_native_mode_no_clock_edp),
+	KUNIT_CASE(dm_test_create_stream_native_mode_no_edid_edp),
 	/* amdgpu_dm_connector_detect */
 	KUNIT_CASE(dm_test_detect_force_on),
 	KUNIT_CASE(dm_test_detect_force_on_digital),
-- 
2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS
  2026-10-24 16:33       ` [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
@ 2026-10-31  2:17         ` Alex Hung
  0 siblings, 0 replies; 9+ messages in thread
From: Alex Hung @ 2026-10-31  2:17 UTC (permalink / raw)
  To: Hugo Sales, amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Chung, ChiaHsuan (Tom)

Reviewed-by: Alex Hung <alex.hung@amd.com>

On 10/24/26 10:33, Hugo Sales wrote:
> [Some people who received this message don't often get email from hugo@hsal.es. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
> 
> Currently, create_stream_for_sink() takes the native timing for a scaled
> mode from the preferred mode in connector->modes. After fbdev emulation
> probes with size limits below the native resolution, as it does when
> video= sets a smaller mode, that mode is no longer in the list
> 
> If that probe is the last one before a suspend, for example after an
> HDMI hotplug, the resume finds no preferred mode, and the panel is
> driven with the scaled mode's timing instead of its own
> 
> amdgpu_dm_get_native_mode() copies the preferred mode to the encoder on
> every probe, before the size limits apply. When connector->modes has no
> preferred mode, take a mode of the native size from the list, or that
> copy if the limits pruned them all. The list comes first because
> mode_valid may have rejected the copy's timing and kept another mode of
> the same size. Limit this to eDP and LVDS, where amdgpu turns scaling on
> by itself. On other connectors mode_valid can also leave the preferred
> mode out, and then the first valid mode is the right fallback
> 
> Closes: https://gitlab.freedesktop.org/drm/amd/-/issues/5983
> Signed-off-by: Hugo Sales <hugo@hsal.es>
> ---
>   .../display/amdgpu_dm/amdgpu_dm_connector.c   | 47 +++++++++++++++++++
>   1 file changed, 47 insertions(+)
> 
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> index ded8e21897f1c..a13e3b641fd27 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_connector.c
> @@ -257,6 +257,8 @@ STATIC_IFN_KUNIT void update_subconnector_property(struct amdgpu_dm_connector *a
>   EXPORT_IF_KUNIT(update_subconnector_property);
> 
>   STATIC_IFN_KUNIT int amdgpu_dm_connector_get_modes(struct drm_connector *connector);
> +STATIC_IFN_KUNIT struct drm_encoder *amdgpu_dm_connector_to_encoder(
> +       struct drm_connector *connector);
> 
>   STATIC_IFN_KUNIT void amdgpu_dm_fbc_init(struct drm_connector *connector)
>   {
> @@ -1545,6 +1547,51 @@ create_stream_for_sink(struct drm_connector *connector,
>                          break;
>                  }
>          }
> +
> +       /*
> +        * After a probe with size limits smaller than the preferred mode,
> +        * connector->modes no longer has that mode, and the first mode left
> +        * would become the stream timing. fbdev emulation probes with limits
> +        * that video= can set below the native size. Only eDP and LVDS panels
> +        * get a fallback here, since they take only their native size. On other
> +        * connectors mode_valid can also reject the preferred mode, and then
> +        * the first valid mode is the right one
> +        *
> +        * Take a mode of the native size from connector->modes if one is left,
> +        * since mode_valid may have rejected the preferred timing itself. The
> +        * probe sorts the list, so the first one has the highest refresh rate
> +        * left. If the limits pruned them all, use the copy of the preferred
> +        * mode that amdgpu_dm_get_native_mode() keeps in the encoder. It is
> +        * made on every probe, before the limits apply
> +        */
> +       if (!preferred_mode && aconnector && aconnector->drm_edid &&
> +           (connector->connector_type == DRM_MODE_CONNECTOR_eDP ||
> +            connector->connector_type == DRM_MODE_CONNECTOR_LVDS)) {
> +               struct drm_encoder *encoder = amdgpu_dm_connector_to_encoder(connector);
> +               const struct drm_display_mode *enc_native =
> +                       encoder ? &to_amdgpu_encoder(encoder)->native_mode : NULL;
> +
> +               if (enc_native && enc_native->clock) {
> +                       list_for_each_entry(connector_mode, &connector->modes, head) {
> +                               if (connector_mode->hdisplay == enc_native->hdisplay &&
> +                                   connector_mode->vdisplay == enc_native->vdisplay) {
> +                                       preferred_mode = connector_mode;
> +                                       break;
> +                               }
> +                       }
> +
> +                       /*
> +                        * saved_mode is only used for FreeSync video modes, and
> +                        * that path doesn't read preferred_mode
> +                        */
> +                       if (!preferred_mode) {
> +                               drm_mode_init(&saved_mode, enc_native);
> +                               drm_mode_set_crtcinfo(&saved_mode, CRTC_INTERLACE_HALVE_V);
> +                               preferred_mode = &saved_mode;
> +                       }
> +               }
> +       }
> +
>          if (!preferred_mode)
>                  preferred_mode = list_first_entry_or_null(
>                                  &connector->modes,
> --
> 2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback
  2026-10-24 16:33       ` [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback Hugo Sales
@ 2026-10-31  2:17         ` Alex Hung
  0 siblings, 0 replies; 9+ messages in thread
From: Alex Hung @ 2026-10-31  2:17 UTC (permalink / raw)
  To: Hugo Sales, amd-gfx
  Cc: dri-devel, linux-kernel, harry.wentland, sunpeng.li, siqueira,
	alexander.deucher, Chung, ChiaHsuan (Tom)

Reviewed-by: Alex Hung <alex.hung@amd.com>

On 10/24/26 10:33, Hugo Sales wrote:
> [Some people who received this message don't often get email from hugo@hsal.es. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
> 
> Leave only a scaled mode in connector->modes and put the larger native
> mode in the encoder, as a size-limited probe does. On eDP and LVDS,
> create_stream_for_sink() must build a stream with the native timing and
> the scaled mode as its source size. On HDMI it must keep the first mode
> in the list as the timing
> 
> When the list also has a mode of the native size with a different
> timing, eDP must use that mode and not the encoder's copy
> 
> The fallback must stay off when the list has a preferred mode, without
> an EDID, and when the encoder's copy has no clock
> 
> Signed-off-by: Hugo Sales <hugo@hsal.es>
> ---
>   .../tests/amdgpu_dm_connector_test.c          | 226 +++++++++++++++++-
>   1 file changed, 224 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
> index d291ae40ca1d0..af71ca660bfea 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_connector_test.c
> @@ -4181,7 +4181,8 @@ struct dm_test_stream_ctx {
>          struct drm_display_mode *mode;
>   };
> 
> -static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
> +static struct dm_test_stream_ctx *
> +dm_test_stream_ctx_alloc_type(struct kunit *test, int connector_type)
>   {
>          struct dm_test_stream_ctx *ctx;
>          struct device *dev;
> @@ -4202,7 +4203,7 @@ static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
>          KUNIT_ASSERT_EQ(test,
>                  drmm_connector_init(ctx->drm, &ctx->aconnector->base,
>                                      &dm_test_connector_funcs,
> -                                   DRM_MODE_CONNECTOR_DisplayPort, NULL), 0);
> +                                   connector_type, NULL), 0);
> 
>          ctx->dc_ctx = kunit_kzalloc(test, sizeof(*ctx->dc_ctx), GFP_KERNEL);
>          KUNIT_ASSERT_NOT_NULL(test, ctx->dc_ctx);
> @@ -4229,6 +4230,11 @@ static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
>          return ctx;
>   }
> 
> +static struct dm_test_stream_ctx *dm_test_stream_ctx_alloc(struct kunit *test)
> +{
> +       return dm_test_stream_ctx_alloc_type(test, DRM_MODE_CONNECTOR_DisplayPort);
> +}
> +
>   /**
>    * dm_test_create_stream_fake_sink_success - Test a stream is built from a fake sink
>    * @test: The KUnit test context
> @@ -4547,6 +4553,215 @@ static void dm_test_create_stream_hdmi_vsif(struct kunit *test)
>          dc_sink_release(sink);
>   }
> 
> +enum dm_test_native {
> +       /*
> +        * The list also keeps a 3840x2400 mode at half the native clock, as if
> +        * mode_valid had rejected the native timing and no limit applied
> +        */
> +       DM_TEST_NATIVE_LISTED = BIT(0),
> +       /* The scaled mode is the preferred one */
> +       DM_TEST_NATIVE_PREFERRED = BIT(1),
> +       DM_TEST_NATIVE_NO_CLOCK = BIT(2),
> +       DM_TEST_NATIVE_NO_EDID = BIT(3),
> +};
> +
> +/*
> + * Build a stream after a probe with size limits below the native mode, such as
> + * fbdev emulation's. That probe leaves only a 1920x1200 scaled mode in the
> + * connector's mode list, while the encoder keeps the 3840x2400 native mode.
> + * @flags is a mask of enum dm_test_native
> + */
> +static struct dc_stream_state *
> +dm_test_native_mode_stream(struct kunit *test, int connector_type, unsigned int flags)
> +{
> +       static const u8 edid[EDID_LENGTH];
> +       struct dm_test_stream_ctx *ctx = dm_test_stream_ctx_alloc_type(test, connector_type);
> +       struct amdgpu_encoder *aenc;
> +       struct dc_stream_state *stream;
> +       struct drm_display_mode *native;
> +
> +       aenc = drmm_kzalloc(ctx->drm, sizeof(*aenc), GFP_KERNEL);
> +       KUNIT_ASSERT_NOT_NULL(test, aenc);
> +       KUNIT_ASSERT_EQ(test, drmm_encoder_init(ctx->drm, &aenc->base, NULL,
> +                                               DRM_MODE_ENCODER_TMDS, NULL), 0);
> +       KUNIT_ASSERT_EQ(test, drm_connector_attach_encoder(&ctx->aconnector->base, &aenc->base), 0);
> +
> +       native = &aenc->native_mode;
> +       native->clock = 592500;
> +       native->hdisplay = 3840;
> +       native->hsync_start = 3888;
> +       native->hsync_end = 3920;
> +       native->htotal = 4000;
> +       native->vdisplay = 2400;
> +       native->vsync_start = 2403;
> +       native->vsync_end = 2409;
> +       native->vtotal = 2469;
> +       /* Cached by the first, unlimited probe */
> +       drm_mode_copy(&ctx->aconnector->freesync_vid_base, native);
> +
> +       drm_mode_copy(ctx->mode, native);
> +       ctx->mode->hdisplay = 1920;
> +       ctx->mode->vdisplay = 1200;
> +       drm_mode_set_crtcinfo(ctx->mode, CRTC_INTERLACE_HALVE_V);
> +       dm_test_stream_add_mode(test, ctx, flags & DM_TEST_NATIVE_PREFERRED ?
> +                               DRM_MODE_TYPE_PREFERRED : 0);
> +
> +       /* After the scaled mode, so that taking the first mode can't pass */
> +       if (flags & DM_TEST_NATIVE_LISTED) {
> +               struct drm_display_mode *listed = drm_mode_duplicate(ctx->drm, ctx->mode);
> +
> +               KUNIT_ASSERT_NOT_NULL(test, listed);
> +               listed->hdisplay = native->hdisplay;
> +               listed->vdisplay = native->vdisplay;
> +               listed->clock /= 2;
> +               drm_mode_set_crtcinfo(listed, CRTC_INTERLACE_HALVE_V);
> +               list_add_tail(&listed->head, &ctx->aconnector->base.modes);
> +       }
> +       ctx->dm_state->scaling = RMX_FULL;
> +
> +       if (flags & DM_TEST_NATIVE_NO_CLOCK)
> +               native->clock = 0;
> +       if (!(flags & DM_TEST_NATIVE_NO_EDID)) {
> +               ctx->aconnector->drm_edid = drm_edid_alloc(edid, sizeof(edid));
> +               KUNIT_ASSERT_NOT_NULL(test, ctx->aconnector->drm_edid);
> +       }
> +
> +       stream = create_stream_for_sink(&ctx->aconnector->base, ctx->mode,
> +                                       ctx->dm_state, NULL, 8,
> +                                       PIXEL_ENCODING_RGB, false);
> +
> +       drm_edid_free(ctx->aconnector->drm_edid);
> +       ctx->aconnector->drm_edid = NULL;
> +       KUNIT_ASSERT_NOT_NULL(test, stream);
> +
> +       return stream;
> +}
> +
> +/* Expect the stream to be driven with the 1920x1200 mode left in the list */
> +static void dm_test_expect_first_mode(struct kunit *test, struct dc_stream_state *stream)
> +{
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 1920);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 1200);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
> +       dc_stream_release(stream);
> +}
> +
> +/**
> + * dm_test_create_stream_native_mode_pruned_edp - Test eDP gets native timing
> + * @test: The KUnit test context
> + *
> + * The scaled mode must still be driven with the panel's native timing, kept in
> + * the encoder
> + */
> +static void dm_test_create_stream_native_mode_pruned_edp(struct kunit *test)
> +{
> +       struct dc_stream_state *stream =
> +               dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP, 0);
> +
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 5925000);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
> +       dc_stream_release(stream);
> +}
> +
> +/**
> + * dm_test_create_stream_native_mode_pruned_lvds - Test LVDS gets native timing
> + * @test: The KUnit test context
> + */
> +static void dm_test_create_stream_native_mode_pruned_lvds(struct kunit *test)
> +{
> +       struct dc_stream_state *stream =
> +               dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_LVDS, 0);
> +
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 5925000);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
> +       dc_stream_release(stream);
> +}
> +
> +/**
> + * dm_test_create_stream_native_mode_pruned_hdmi - Test HDMI uses the first mode
> + * @test: The KUnit test context
> + *
> + * On a connector that is not a panel, mode_valid may have dropped the native
> + * mode, so the first mode left in the list must drive the timing
> + */
> +static void dm_test_create_stream_native_mode_pruned_hdmi(struct kunit *test)
> +{
> +       dm_test_expect_first_mode(test,
> +                                 dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_HDMIA, 0));
> +}
> +
> +/**
> + * dm_test_create_stream_native_size_listed_edp - Test eDP prefers a listed mode
> + * @test: The KUnit test context
> + *
> + * When the mode list still has a mode of the native size, that mode's timing
> + * must drive the panel instead of the encoder's copy, which mode_valid may
> + * have rejected
> + */
> +static void dm_test_create_stream_native_size_listed_edp(struct kunit *test)
> +{
> +       struct dc_stream_state *stream =
> +               dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
> +                                          DM_TEST_NATIVE_LISTED);
> +
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.h_addressable, 3840);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.v_addressable, 2400);
> +       KUNIT_EXPECT_EQ(test, (int)stream->timing.pix_clk_100hz, 2962500);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.width, 1920);
> +       KUNIT_EXPECT_EQ(test, (int)stream->src.height, 1200);
> +       dc_stream_release(stream);
> +}
> +
> +/**
> + * dm_test_create_stream_native_preferred_edp - Test a listed preferred mode
> + * wins on eDP
> + * @test: The KUnit test context
> + */
> +static void dm_test_create_stream_native_preferred_edp(struct kunit *test)
> +{
> +       dm_test_expect_first_mode(test,
> +                                 dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
> +                                                            DM_TEST_NATIVE_LISTED |
> +                                                            DM_TEST_NATIVE_PREFERRED));
> +}
> +
> +/**
> + * dm_test_create_stream_native_mode_no_clock_edp - Test an empty native mode
> + * isn't used
> + * @test: The KUnit test context
> + *
> + * amdgpu_dm_get_native_mode() zeroes the clock and then copies only a
> + * preferred mode at the head of probed_modes. When there is none, the size
> + * left in the encoder is stale and must not pick a listed mode
> + */
> +static void dm_test_create_stream_native_mode_no_clock_edp(struct kunit *test)
> +{
> +       dm_test_expect_first_mode(test,
> +                                 dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
> +                                                            DM_TEST_NATIVE_LISTED |
> +                                                            DM_TEST_NATIVE_NO_CLOCK));
> +}
> +
> +/**
> + * dm_test_create_stream_native_mode_no_edid_edp - Test the fallback needs an
> + * EDID
> + * @test: The KUnit test context
> + */
> +static void dm_test_create_stream_native_mode_no_edid_edp(struct kunit *test)
> +{
> +       dm_test_expect_first_mode(test,
> +                                 dm_test_native_mode_stream(test, DRM_MODE_CONNECTOR_eDP,
> +                                                            DM_TEST_NATIVE_LISTED |
> +                                                            DM_TEST_NATIVE_NO_EDID));
> +}
> +
>   /* Tests for amdgpu_dm_connector_detect() */
> 
>   /*
> @@ -10248,6 +10463,13 @@ static struct kunit_case amdgpu_dm_connector_tests[] = {
>          KUNIT_CASE(dm_test_create_stream_dp_vsc_colorimetry),
>          KUNIT_CASE(dm_test_create_stream_dp_colorimetry_quirk),
>          KUNIT_CASE(dm_test_create_stream_hdmi_vsif),
> +       KUNIT_CASE(dm_test_create_stream_native_mode_pruned_edp),
> +       KUNIT_CASE(dm_test_create_stream_native_mode_pruned_lvds),
> +       KUNIT_CASE(dm_test_create_stream_native_mode_pruned_hdmi),
> +       KUNIT_CASE(dm_test_create_stream_native_size_listed_edp),
> +       KUNIT_CASE(dm_test_create_stream_native_preferred_edp),
> +       KUNIT_CASE(dm_test_create_stream_native_mode_no_clock_edp),
> +       KUNIT_CASE(dm_test_create_stream_native_mode_no_edid_edp),
>          /* amdgpu_dm_connector_detect */
>          KUNIT_CASE(dm_test_detect_force_on),
>          KUNIT_CASE(dm_test_detect_force_on_digital),
> --
> 2.55.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-10-31  2:17 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-14 22:30 [PATCH 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
2026-10-14 22:30 ` [PATCH 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
2026-10-23 18:26   ` Chung, ChiaHsuan (Tom)
2026-10-24 16:33     ` [PATCH v2 0/2] drm/amd/display: Keep eDP native timing after resume with video= Hugo Sales
2026-10-24 16:33       ` [PATCH v2 1/2] drm/amd/display: Fall back to the native mode, not the first, on eDP/LVDS Hugo Sales
2026-10-31  2:17         ` Alex Hung
2026-10-24 16:33       ` [PATCH v2 2/2] drm/amd/display: Add KUnit tests for the eDP/LVDS native mode fallback Hugo Sales
2026-10-31  2:17         ` Alex Hung
2026-10-14 22:30 ` [PATCH " Hugo Sales

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®