* [PATCH 1/2] drm/client: Probe connectors with the device's mode size limits
2026-10-15 21:25 [PATCH 0/2] drm/client: Keep larger modes in connector->modes on fbdev hotplug Hugo Sales
@ 2026-10-15 21:25 ` Hugo Sales
2026-10-15 21:25 ` [PATCH 2/2] drm/tests: Add KUnit tests for the client " Hugo Sales
2026-10-16 12:28 ` [PATCH 0/2] drm/client: Keep larger modes in connector->modes on fbdev hotplug Ville Syrjälä
2 siblings, 0 replies; 8+ messages in thread
From: Hugo Sales @ 2026-10-15 21:25 UTC (permalink / raw)
To: dri-devel
Cc: linux-kernel, maarten.lankhorst, mripard, tzimmermann, airlied,
simona, ville.syrjala, jfalempe, javierm, Hugo Sales
Currently, drm_client_modeset_probe() passes the client's width and
height to ->fill_modes(), and the probe helper leaves every larger mode
out of connector->modes. drm_log, fbdev's initial setup and the
GETCONNECTOR ioctl already probe with the device's limits, but on a
hotplug fbdev passes its framebuffer size. After that, the list that
userspace and the driver share lacks every mode larger than the
framebuffer. That includes the panel's preferred mode when video= sets
a smaller one
Probe with the device's limits instead. Make the mode pickers skip
modes that don't fit width x height, using the drm_mode_validate_size()
check the probe helper applied before. The probe helper still runs
mode_valid on every mode, so only the size check moves
fbdev picks the same modes as before, with one exception. A DisplayPort
sink with no mode that fits used to end up with an empty list, so the
probe helper added its 640x480 fallback modes. Its list now keeps the
larger modes, and fbdev leaves that output off
Link: https://gitlab.freedesktop.org/drm/amd/-/issues/5983
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
drivers/gpu/drm/drm_client_modeset.c | 57 +++++++++++++------
.../gpu/drm/tests/drm_client_modeset_test.c | 4 +-
2 files changed, 43 insertions(+), 18 deletions(-)
diff --git a/drivers/gpu/drm/drm_client_modeset.c b/drivers/gpu/drm/drm_client_modeset.c
index 0080a8e9556a..e79c7257aaa2 100644
--- a/drivers/gpu/drm/drm_client_modeset.c
+++ b/drivers/gpu/drm/drm_client_modeset.c
@@ -121,11 +121,13 @@ drm_client_find_modeset(struct drm_client_dev *client, struct drm_crtc *crtc)
}
static const struct drm_display_mode *
-drm_connector_get_tiled_mode(struct drm_connector *connector)
+drm_connector_get_tiled_mode(struct drm_connector *connector, int width, int height)
{
const struct drm_display_mode *mode;
list_for_each_entry(mode, &connector->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) != MODE_OK)
+ continue;
if (mode->hdisplay == connector->tile_h_size &&
mode->vdisplay == connector->tile_v_size)
return mode;
@@ -134,11 +136,13 @@ drm_connector_get_tiled_mode(struct drm_connector *connector)
}
static const struct drm_display_mode *
-drm_connector_fallback_non_tiled_mode(struct drm_connector *connector)
+drm_connector_fallback_non_tiled_mode(struct drm_connector *connector, int width, int height)
{
const struct drm_display_mode *mode;
list_for_each_entry(mode, &connector->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) != MODE_OK)
+ continue;
if (mode->hdisplay == connector->tile_h_size &&
mode->vdisplay == connector->tile_v_size)
continue;
@@ -163,14 +167,19 @@ drm_connector_preferred_mode(struct drm_connector *connector, int width, int hei
}
static const struct drm_display_mode *
-drm_connector_first_mode(struct drm_connector *connector)
+drm_connector_first_mode(struct drm_connector *connector, int width, int height)
{
- return list_first_entry_or_null(&connector->modes,
- struct drm_display_mode, head);
+ const struct drm_display_mode *mode;
+
+ list_for_each_entry(mode, &connector->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) == MODE_OK)
+ return mode;
+ }
+ return NULL;
}
static const struct drm_display_mode *
-drm_connector_pick_cmdline_mode(struct drm_connector *connector)
+drm_connector_pick_cmdline_mode(struct drm_connector *connector, int width, int height)
{
const struct drm_cmdline_mode *cmdline_mode;
const struct drm_display_mode *mode;
@@ -183,6 +192,8 @@ drm_connector_pick_cmdline_mode(struct drm_connector *connector)
*/
list_for_each_entry(mode, &connector->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) != MODE_OK)
+ continue;
if (mode->type & DRM_MODE_TYPE_USERDEF)
return mode;
}
@@ -199,6 +210,8 @@ drm_connector_pick_cmdline_mode(struct drm_connector *connector)
prefer_non_interlace = !cmdline_mode->interlace;
again:
list_for_each_entry(mode, &connector->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) != MODE_OK)
+ continue;
/* check width/height */
if (mode->hdisplay != cmdline_mode->xres ||
mode->vdisplay != cmdline_mode->yres)
@@ -321,7 +334,7 @@ static bool drm_client_target_cloned(struct drm_device *dev,
continue;
mode_replace(dev, &modes[i],
- drm_connector_pick_cmdline_mode(connectors[i]));
+ drm_connector_pick_cmdline_mode(connectors[i], width, height));
if (!modes[i]) {
can_clone = false;
break;
@@ -357,6 +370,8 @@ static bool drm_client_target_cloned(struct drm_device *dev,
continue;
list_for_each_entry(mode, &connectors[i]->modes, head) {
+ if (drm_mode_validate_size(mode, width, height) != MODE_OK)
+ continue;
if (drm_mode_match(mode, dmt_mode,
DRM_MODE_MATCH_TIMINGS |
DRM_MODE_MATCH_CLOCK |
@@ -472,7 +487,7 @@ static bool drm_client_target_preferred(struct drm_device *dev,
mode_type = "cmdline";
mode_replace(dev, &modes[i],
- drm_connector_pick_cmdline_mode(connector));
+ drm_connector_pick_cmdline_mode(connector, width, height));
if (!modes[i]) {
mode_type = "preferred";
@@ -483,7 +498,7 @@ static bool drm_client_target_preferred(struct drm_device *dev,
if (!modes[i]) {
mode_type = "first";
mode_replace(dev, &modes[i],
- drm_connector_first_mode(connector));
+ drm_connector_first_mode(connector, width, height));
}
/*
@@ -499,14 +514,16 @@ static bool drm_client_target_preferred(struct drm_device *dev,
connector->num_h_tile * connector->num_v_tile ||
(connector->tile_h_loc == 0 &&
connector->tile_v_loc == 0 &&
- !drm_connector_get_tiled_mode(connector))) {
+ !drm_connector_get_tiled_mode(connector, width, height))) {
mode_type = "non tiled";
mode_replace(dev, &modes[i],
- drm_connector_fallback_non_tiled_mode(connector));
+ drm_connector_fallback_non_tiled_mode(connector,
+ width, height));
} else {
mode_type = "tiled";
mode_replace(dev, &modes[i],
- drm_connector_get_tiled_mode(connector));
+ drm_connector_get_tiled_mode(connector,
+ width, height));
}
}
@@ -721,7 +738,7 @@ static bool drm_client_firmware_config(struct drm_client_dev *client,
mode_type = "cmdline";
mode_replace(dev, &modes[i],
- drm_connector_pick_cmdline_mode(connector));
+ drm_connector_pick_cmdline_mode(connector, width, height));
if (!modes[i]) {
mode_type = "preferred";
@@ -732,7 +749,7 @@ static bool drm_client_firmware_config(struct drm_client_dev *client,
if (!modes[i]) {
mode_type = "first";
mode_replace(dev, &modes[i],
- drm_connector_first_mode(connector));
+ drm_connector_first_mode(connector, width, height));
}
/* last resort: use current mode */
@@ -750,7 +767,8 @@ static bool drm_client_firmware_config(struct drm_client_dev *client,
num_tiled_conns < connector->num_h_tile * connector->num_v_tile) {
mode_type = "non tiled";
mode_replace(dev, &modes[i],
- drm_connector_fallback_non_tiled_mode(connector));
+ drm_connector_fallback_non_tiled_mode(connector,
+ width, height));
}
crtcs[i] = crtc;
@@ -865,8 +883,15 @@ int drm_client_modeset_probe(struct drm_client_dev *client, unsigned int width,
mutex_lock(&client->modeset_mutex);
mutex_lock(&dev->mode_config.mutex);
+ /*
+ * Probe with the device limits, not width x height. connector->modes is
+ * shared with userspace and the driver, so it must keep modes larger
+ * than this client's size. The mode pickers skip those instead
+ */
for (i = 0; i < connector_count; i++)
- total_modes_count += connectors[i]->funcs->fill_modes(connectors[i], width, height);
+ total_modes_count += connectors[i]->funcs->fill_modes(connectors[i],
+ dev->mode_config.max_width,
+ dev->mode_config.max_height);
if (!total_modes_count)
drm_dbg_kms(dev, "No connectors reported connected with modes\n");
drm_client_connectors_enabled(connectors, connector_count, enabled);
diff --git a/drivers/gpu/drm/tests/drm_client_modeset_test.c b/drivers/gpu/drm/tests/drm_client_modeset_test.c
index ec58fe064d86..00f43fca9795 100644
--- a/drivers/gpu/drm/tests/drm_client_modeset_test.c
+++ b/drivers/gpu/drm/tests/drm_client_modeset_test.c
@@ -112,7 +112,7 @@ static void drm_test_pick_cmdline_res_1920_1080_60(struct kunit *test)
mutex_unlock(&drm->mode_config.mutex);
KUNIT_ASSERT_GT(test, ret, 0);
- mode = drm_connector_pick_cmdline_mode(connector);
+ mode = drm_connector_pick_cmdline_mode(connector, 1920, 1080);
KUNIT_ASSERT_NOT_NULL(test, mode);
KUNIT_EXPECT_TRUE(test, drm_mode_equal(expected_mode, mode));
@@ -151,7 +151,7 @@ static void drm_test_pick_cmdline_named(struct kunit *test)
mutex_unlock(&drm->mode_config.mutex);
KUNIT_ASSERT_GT(test, ret, 0);
- mode = drm_connector_pick_cmdline_mode(connector);
+ mode = drm_connector_pick_cmdline_mode(connector, 1920, 1080);
KUNIT_ASSERT_NOT_NULL(test, mode);
expected_mode = params->func(drm);
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH 2/2] drm/tests: Add KUnit tests for the client mode size limits
2026-10-15 21:25 [PATCH 0/2] drm/client: Keep larger modes in connector->modes on fbdev hotplug Hugo Sales
2026-10-15 21:25 ` [PATCH 1/2] drm/client: Probe connectors with the device's mode size limits Hugo Sales
@ 2026-10-15 21:25 ` Hugo Sales
2026-10-16 12:28 ` [PATCH 0/2] drm/client: Keep larger modes in connector->modes on fbdev hotplug Ville Syrjälä
2 siblings, 0 replies; 8+ messages in thread
From: Hugo Sales @ 2026-10-15 21:25 UTC (permalink / raw)
To: dri-devel
Cc: linux-kernel, maarten.lankhorst, mripard, tzimmermann, airlied,
simona, ville.syrjala, jfalempe, javierm, Hugo Sales
Test that the first-mode, cmdline, tiled and clone pickers skip modes
larger than the client's size, and that drm_client_modeset_probe() keeps
the preferred mode in connector->modes when the client is smaller than
it
The probe test calls drm_client_modeset_create() directly, because
drm_client_init() needs a dumb_create callback the test driver doesn't
have
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Hugo Sales <hugo@hsal.es>
---
.../gpu/drm/tests/drm_client_modeset_test.c | 218 +++++++++++++++++-
1 file changed, 217 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/tests/drm_client_modeset_test.c b/drivers/gpu/drm/tests/drm_client_modeset_test.c
index 00f43fca9795..93aa919d4f31 100644
--- a/drivers/gpu/drm/tests/drm_client_modeset_test.c
+++ b/drivers/gpu/drm/tests/drm_client_modeset_test.c
@@ -18,6 +18,8 @@ struct drm_client_modeset_test_priv {
struct drm_device *drm;
struct device *dev;
struct drm_connector connector;
+ struct drm_connector second_connector;
+ struct drm_encoder encoder;
};
static int drm_client_modeset_connector_get_modes(struct drm_connector *connector)
@@ -48,9 +50,23 @@ static const struct drm_connector_helper_funcs drm_client_modeset_connector_help
.get_modes = drm_client_modeset_connector_get_modes,
};
+static int drm_client_modeset_connector_get_modes_preferred(struct drm_connector *connector)
+{
+ int count = drm_add_modes_noedid(connector, 1920, 1080);
+
+ drm_set_preferred_mode(connector, 1920, 1080);
+
+ return count;
+}
+
+static const struct drm_connector_helper_funcs drm_client_modeset_preferred_helper_funcs = {
+ .get_modes = drm_client_modeset_connector_get_modes_preferred,
+};
+
static const struct drm_connector_funcs drm_client_modeset_connector_funcs = {
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
- .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state
+ .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+ .fill_modes = drm_helper_probe_single_connector_modes,
};
static int drm_client_modeset_test_init(struct kunit *test)
@@ -197,6 +213,206 @@ static struct kunit_suite drm_test_pick_cmdline_test_suite = {
kunit_test_suite(drm_test_pick_cmdline_test_suite);
+static void drm_test_pick_first_mode_within_limits(struct kunit *test)
+{
+ struct drm_client_modeset_test_priv *priv = test->priv;
+ struct drm_device *drm = priv->drm;
+ struct drm_connector *connector = &priv->connector;
+ const struct drm_display_mode *mode;
+ int ret;
+
+ drm_connector_helper_add(connector, &drm_client_modeset_preferred_helper_funcs);
+
+ mutex_lock(&drm->mode_config.mutex);
+ ret = drm_helper_probe_single_connector_modes(connector, 1920, 1080);
+ mutex_unlock(&drm->mode_config.mutex);
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ mode = drm_connector_first_mode(connector, 1920, 1080);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1920);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 1080);
+
+ KUNIT_ASSERT_NULL(test, drm_connector_preferred_mode(connector, 1024, 768));
+
+ mode = drm_connector_first_mode(connector, 1024, 768);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1024);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 768);
+
+ KUNIT_EXPECT_NULL(test, drm_connector_first_mode(connector, 320, 240));
+}
+
+static void drm_test_pick_cmdline_mode_within_limits(struct kunit *test)
+{
+ struct drm_client_modeset_test_priv *priv = test->priv;
+ struct drm_device *drm = priv->drm;
+ struct drm_connector *connector = &priv->connector;
+ struct drm_cmdline_mode *cmdline_mode = &connector->cmdline_mode;
+ const struct drm_display_mode *mode;
+ int ret;
+
+ KUNIT_ASSERT_TRUE(test, drm_mode_parse_command_line_for_connector("1920x1080", connector,
+ cmdline_mode));
+
+ mutex_lock(&drm->mode_config.mutex);
+ ret = drm_helper_probe_single_connector_modes(connector, 1920, 1080);
+ mutex_unlock(&drm->mode_config.mutex);
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ mode = drm_connector_pick_cmdline_mode(connector, 1920, 1080);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1920);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 1080);
+
+ KUNIT_EXPECT_NULL(test, drm_connector_pick_cmdline_mode(connector, 1024, 768));
+}
+
+static void drm_test_pick_tiled_mode_within_limits(struct kunit *test)
+{
+ struct drm_client_modeset_test_priv *priv = test->priv;
+ struct drm_device *drm = priv->drm;
+ struct drm_connector *connector = &priv->connector;
+ const struct drm_display_mode *mode;
+ int ret;
+
+ drm_connector_helper_add(connector, &drm_client_modeset_preferred_helper_funcs);
+ connector->tile_h_size = 1920;
+ connector->tile_v_size = 1080;
+
+ mutex_lock(&drm->mode_config.mutex);
+ ret = drm_helper_probe_single_connector_modes(connector, 1920, 1080);
+ mutex_unlock(&drm->mode_config.mutex);
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ mode = drm_connector_get_tiled_mode(connector, 1920, 1080);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1920);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 1080);
+
+ KUNIT_EXPECT_NULL(test, drm_connector_get_tiled_mode(connector, 1024, 768));
+
+ mode = drm_connector_fallback_non_tiled_mode(connector, 1024, 768);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1024);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 768);
+}
+
+static void drm_test_pick_clone_mode_within_limits(struct kunit *test)
+{
+ struct drm_client_modeset_test_priv *priv = test->priv;
+ struct drm_device *drm = priv->drm;
+ struct drm_connector *connectors[] = { &priv->connector, &priv->second_connector };
+ const struct drm_display_mode *modes[ARRAY_SIZE(connectors)] = {};
+ struct drm_client_offset offsets[ARRAY_SIZE(connectors)] = {};
+ bool enabled[ARRAY_SIZE(connectors)] = { true, true };
+ unsigned int i;
+ int ret;
+
+ ret = drmm_connector_init(drm, &priv->second_connector,
+ &drm_client_modeset_connector_funcs,
+ DRM_MODE_CONNECTOR_Unknown,
+ NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ drm_connector_helper_add(&priv->second_connector,
+ &drm_client_modeset_connector_helper_funcs);
+
+ mutex_lock(&drm->mode_config.mutex);
+ for (i = 0; i < ARRAY_SIZE(connectors); i++) {
+ ret = drm_helper_probe_single_connector_modes(connectors[i], 1920, 1080);
+ KUNIT_EXPECT_GT(test, ret, 0);
+ }
+ mutex_unlock(&drm->mode_config.mutex);
+
+ KUNIT_EXPECT_TRUE(test, drm_client_target_cloned(drm, connectors, ARRAY_SIZE(connectors),
+ modes, offsets, enabled, 1024, 768));
+ for (i = 0; i < ARRAY_SIZE(connectors); i++) {
+ KUNIT_ASSERT_NOT_NULL(test, modes[i]);
+ KUNIT_EXPECT_EQ(test, modes[i]->hdisplay, 1024);
+ KUNIT_EXPECT_EQ(test, modes[i]->vdisplay, 768);
+ }
+ modes_destroy(drm, modes, ARRAY_SIZE(connectors));
+
+ KUNIT_EXPECT_FALSE(test, drm_client_target_cloned(drm, connectors, ARRAY_SIZE(connectors),
+ modes, offsets, enabled, 800, 600));
+ modes_destroy(drm, modes, ARRAY_SIZE(connectors));
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(drm_client_modeset_free_wrapper, drm_client_modeset_free,
+ struct drm_client_dev *);
+
+static void drm_test_probe_keeps_modes_above_limits(struct kunit *test)
+{
+ struct drm_client_modeset_test_priv *priv = test->priv;
+ struct drm_device *drm = priv->drm;
+ struct drm_connector *connector = &priv->connector;
+ const struct drm_display_mode *mode;
+ struct drm_client_dev *client;
+ struct drm_encoder *encoder = &priv->encoder;
+ struct drm_plane *plane;
+ struct drm_crtc *crtc;
+ int ret;
+
+ drm->mode_config.max_width = 4096;
+ drm->mode_config.max_height = 4096;
+
+ plane = drm_kunit_helper_create_primary_plane(test, drm, NULL, NULL, NULL, 0, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, plane);
+
+ crtc = drm_kunit_helper_create_crtc(test, drm, plane, NULL, NULL, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, crtc);
+
+ ret = drmm_encoder_init(drm, encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ encoder->possible_crtcs = drm_crtc_mask(crtc);
+
+ ret = drm_connector_attach_encoder(connector, encoder);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ drm_connector_helper_add(connector, &drm_client_modeset_preferred_helper_funcs);
+
+ client = kunit_kzalloc(test, sizeof(*client), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, client);
+ client->dev = drm;
+
+ ret = drm_client_modeset_create(client);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_add_action_or_reset(test, drm_client_modeset_free_wrapper, client);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = drm_client_modeset_probe(client, 1024, 768);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ mode = client->modesets[0].mode;
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1024);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 768);
+
+ mode = drm_connector_preferred_mode(connector, 1920, 1080);
+ KUNIT_ASSERT_NOT_NULL(test, mode);
+ KUNIT_EXPECT_EQ(test, mode->hdisplay, 1920);
+ KUNIT_EXPECT_EQ(test, mode->vdisplay, 1080);
+}
+
+static struct kunit_case drm_test_pick_mode_limits_tests[] = {
+ KUNIT_CASE(drm_test_pick_first_mode_within_limits),
+ KUNIT_CASE(drm_test_pick_cmdline_mode_within_limits),
+ KUNIT_CASE(drm_test_pick_tiled_mode_within_limits),
+ KUNIT_CASE(drm_test_pick_clone_mode_within_limits),
+ KUNIT_CASE(drm_test_probe_keeps_modes_above_limits),
+ {}
+};
+
+static struct kunit_suite drm_test_pick_mode_limits_test_suite = {
+ .name = "drm_test_pick_mode_limits",
+ .init = drm_client_modeset_test_init,
+ .test_cases = drm_test_pick_mode_limits_tests
+};
+
+kunit_test_suite(drm_test_pick_mode_limits_test_suite);
+
/*
* This file is included directly by drm_client_modeset.c so we can't
* use any MODULE_* macro here.
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread