From 2628715f02cd3dbaeaaa0fcd86a2053ce0c32515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Dvo=C5=99=C3=A1k?= Date: Tue, 28 Apr 2026 20:26:20 +0200 Subject: [PATCH 1/2] meta-monitor-manager.c: Preserve current config across hotplug events (#819) Hotplug events that don't change the connected monitor set (e.g. a monitor's auto input scan triggering a DRM link-state ping) caused ensure_configured() to walk its fallback chain and end up at create_suggested(), which re-enabled monitors the user had explicitly turned off via xrandr, Super+P, or the Display GUI. The user's most recent intent only lives in current_config (set on every successful apply, including TEMPORARY ones), and that was never consulted. Insert a current_config-preferring branch at the top of ensure_configured(): if there is an applied config and it is still complete for the detected monitor set, re-apply it. is_config_complete checks the monitor-spec key (connector + EDID), so genuine plug/unplug falls through to the existing logic unchanged. Skipped during in_init so the on-disk stored config still wins on session start. Also guard set_current() against the no-op self-set, otherwise every hotplug would push a duplicate of the current config onto the 3-slot history queue and evict the genuine previous configs. --- src/backends/meta-monitor-config-manager.c | 3 ++ src/backends/meta-monitor-manager.c | 33 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/backends/meta-monitor-config-manager.c b/src/backends/meta-monitor-config-manager.c index a42d200e2..941bfea10 100644 --- a/src/backends/meta-monitor-config-manager.c +++ b/src/backends/meta-monitor-config-manager.c @@ -1447,6 +1447,9 @@ void meta_monitor_config_manager_set_current (MetaMonitorConfigManager *config_manager, MetaMonitorsConfig *config) { + if (config_manager->current_config == config) + return; + if (config_manager->current_config) { g_queue_push_head (&config_manager->config_history, diff --git a/src/backends/meta-monitor-manager.c b/src/backends/meta-monitor-manager.c index 8abaa8368..fb411123e 100644 --- a/src/backends/meta-monitor-manager.c +++ b/src/backends/meta-monitor-manager.c @@ -696,6 +696,39 @@ meta_monitor_manager_ensure_configured (MetaMonitorManager *manager) else method = META_MONITORS_CONFIG_METHOD_TEMPORARY; + /* If a configuration has already been applied this session and is still + * valid for the detected monitor set, re-apply it. This preserves user + * intent across hotplug events that do not actually change the connected + * monitors — e.g. spurious link-state pings caused by a monitor's input + * auto-scan, or short DP/HDMI re-trains. Without this, the fallbacks + * below would compute a fresh "suggested" layout and silently re-enable + * a monitor the user has explicitly turned off via xrandr, Super+P, or + * the Display GUI. The stored on-disk configuration is consulted only + * during initial setup (current_config is NULL until the first apply). + */ + if (!manager->in_init) + { + MetaMonitorsConfig *current_config = + meta_monitor_config_manager_get_current (manager->config_manager); + + if (current_config && + meta_monitor_manager_is_config_complete (manager, current_config)) + { + if (meta_monitor_manager_apply_monitors_config (manager, + current_config, + method, + &error)) + { + config = g_object_ref (current_config); + goto done; + } + + g_warning ("Failed to re-apply current monitor configuration: %s", + error->message); + g_clear_error (&error); + } + } + if (use_stored_config) { g_autoptr(MetaMonitorsConfig) new_config = NULL; From 8089225dc8b8c2ea445e0350a7e87b10f187d2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Dvo=C5=99=C3=A1k?= Date: Tue, 28 Apr 2026 20:57:38 +0200 Subject: [PATCH 2/2] tests/monitor: Add regression test for issue #819 hotplug-preserves-disabled Build a config with the second monitor disabled, mark it current (as apply_monitors_config does after a TEMPORARY apply), then trigger a hotplug with the same monitor topology. Before the fix this re-built a fresh suggested layout and re-enabled the monitor; after the fix the disabled state survives. --- src/tests/monitor-unit-tests.c | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/tests/monitor-unit-tests.c b/src/tests/monitor-unit-tests.c index 8e4166243..66c581270 100644 --- a/src/tests/monitor-unit-tests.c +++ b/src/tests/monitor-unit-tests.c @@ -6297,6 +6297,84 @@ meta_test_monitor_migrated_wiggle (void) g_error ("Failed to remove test data output file: %s", error->message); } +/* + * Regression test for issue #819: a hotplug event on an unchanged + * connected-monitor set must not discard a user-applied configuration. + * Concretely, a monitor that the user has explicitly disabled (e.g. + * via Super+P, xrandr --off, or the Display GUI) must remain disabled + * after the kernel emits a DRM hotplug — most often caused by the + * monitor itself performing an auto input-scan and re-toggling its + * link state. + * + * Without the fix, ensure_configured() walked past current_config and + * fell through to create_suggested(), which silently re-enabled every + * detected output. + */ +static void +meta_test_monitor_hotplug_preserves_user_disabled_monitor (void) +{ + MetaBackend *backend = meta_get_backend (); + MetaMonitorManager *monitor_manager = + meta_backend_get_monitor_manager (backend); + MetaMonitorConfigManager *config_manager = monitor_manager->config_manager; + MonitorTestCase test_case = initial_test_case; + MetaMonitorTestSetup *test_setup; + MetaMonitorsConfig *linear; + MetaMonitorsConfig *disabled_config; + MetaMonitorsConfig *post_hotplug_config; + MetaLogicalMonitorLayoutMode layout_mode; + GList *first_link; + + /* Initial hotplug: both monitors detected, both enabled. */ + test_setup = create_monitor_test_setup (&test_case, + MONITOR_TEST_FLAG_NO_STORED); + emulate_hotplug (test_setup); + + /* Build a "second monitor disabled" config by stealing the first + * logical_monitor_config out of a freshly-created linear config and + * handing it to meta_monitors_config_new(), which auto-populates + * disabled_monitor_specs from the remaining detected monitors. The + * remaining list elements are released when the linear config is + * dropped. */ + linear = meta_monitor_config_manager_create_linear (config_manager); + layout_mode = linear->layout_mode; + first_link = linear->logical_monitor_configs; + linear->logical_monitor_configs = first_link->next; + if (first_link->next) + first_link->next->prev = NULL; + first_link->next = NULL; + g_object_unref (linear); + + disabled_config = meta_monitors_config_new (monitor_manager, + first_link, + layout_mode, + META_MONITORS_CONFIG_FLAG_NONE); + g_assert_cmpuint (g_list_length (disabled_config->logical_monitor_configs), + ==, 1); + g_assert_cmpuint (g_list_length (disabled_config->disabled_monitor_specs), + ==, 1); + + /* Mark it as current — this is what apply_monitors_config() does on + * a successful TEMPORARY apply (the path taken by xrandr --off and + * Super+P, neither of which writes to the on-disk config store). */ + meta_monitor_config_manager_set_current (config_manager, disabled_config); + g_object_unref (disabled_config); + + /* Hotplug with the same monitor topology — simulates a monitor's + * auto input-scan triggering a DRM link-state ping. */ + test_setup = create_monitor_test_setup (&test_case, + MONITOR_TEST_FLAG_NO_STORED); + emulate_hotplug (test_setup); + + post_hotplug_config = + meta_monitor_config_manager_get_current (config_manager); + g_assert_nonnull (post_hotplug_config); + g_assert_cmpuint (g_list_length (post_hotplug_config->logical_monitor_configs), + ==, 1); + g_assert_cmpuint (g_list_length (post_hotplug_config->disabled_monitor_specs), + ==, 1); +} + static void test_case_setup (void **fixture, const void *data) @@ -6418,6 +6496,9 @@ init_monitor_tests (void) add_monitor_test ("/backends/monitor/wm/tiling", meta_test_monitor_wm_tiling); + + add_monitor_test ("/backends/monitor/hotplug-preserves-user-disabled-monitor", + meta_test_monitor_hotplug_preserves_user_disabled_monitor); } void