From 0ca451483fa493750035b86bd3d19d109020ed8b Mon Sep 17 00:00:00 2001 From: Rocho Date: Wed, 17 Jun 2026 04:58:21 +0200 Subject: [PATCH] fix(shell): don't treat DPMS off/on as a screen-reconnect recovery storm (#2654) A DPMS off/on cycle removes an output from Quickshell.screens and re-adds it, which DMSShell's onScreensChanged cannot distinguish from a hotplug. It fired triggerSurfaceRecovery() on every such event; on hardware where recreating layer-shell surfaces re-wakes the just-powered-down output, this drives an endless recovery storm that visibly power-cycles the monitor. Route the screen-reconnect path through a 450 ms debounce (collapsing the output-remove + re-add pair into a single pass) followed by a 4 s cooldown, so repeated flaps trigger at most one recovery per window. Recovery still runs once per resume, so the partial-DPMS-resume recovery added for #2579 is preserved. The session-resume path runs its own recovery directly and now clears any queued screen-reconnect recovery to avoid a redundant follow-up. Fixes #2642 --- quickshell/DMSShell.qml | 60 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 30500b99..23726af2 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -323,6 +323,9 @@ Item { property bool hadRealScreen: true property var previousRealScreenNames: [] + // Guards for the screen-reconnect recovery path (see scheduleScreenReconnectRecovery). + property bool _screenRecoveryCooldown: false + property bool _screenRecoveryPending: false function _getRealScreenNames() { const names = []; @@ -365,15 +368,60 @@ Item { const partialReconnect = root.previousRealScreenNames.length > 0 && currentNames.some(name => !root.previousRealScreenNames.includes(name)); if (fullReconnect || partialReconnect) { - log.info("Screen reconnect detected, triggering surface recovery", + log.info("Screen reconnect detected, scheduling surface recovery", "full:", fullReconnect, "partial:", partialReconnect); - root.triggerSurfaceRecovery("screen-reconnect"); + root.scheduleScreenReconnectRecovery(); } root.hadRealScreen = hasReal; root.previousRealScreenNames = currentNames; } } + // A DPMS off/on cycle removes an output from the screen list and re-adds it, + // which is indistinguishable here from a hotplug. Recovering immediately on + // every such event lets a flapping monitor (or a recovery that itself perturbs + // the output) drive an endless recovery storm that power-cycles the display + // (#2642). Debounce a burst of changes into a single pass, then hold a cooldown + // so repeated flaps trigger at most one recovery per window. Recovery still runs + // once per resume, so a partial DPMS resume keeps redrawing its surfaces (#2579). + function scheduleScreenReconnectRecovery() { + if (root._screenRecoveryCooldown) { + root._screenRecoveryPending = true; + return; + } + screenReconnectDebounce.restart(); + } + + Timer { + id: screenReconnectDebounce + // Wide enough to collapse the output-remove + output-re-add pair that one + // DPMS off/on cycle emits as two near-simultaneous events into one recovery. + interval: 450 + repeat: false + onTriggered: { + root._screenRecoveryCooldown = true; + root._screenRecoveryPending = false; + screenReconnectCooldown.restart(); + root.triggerSurfaceRecovery("screen-reconnect"); + } + } + + Timer { + id: screenReconnectCooldown + // Must exceed the full two-pass surfaceResumeRecoveryTimer sequence + // (800 + 2000 ms) so the cooldown still covers an in-flight recovery; + // raise this if those passes are lengthened. + interval: 4000 + repeat: false + onTriggered: { + root._screenRecoveryCooldown = false; + if (root._screenRecoveryPending) { + root._screenRecoveryPending = false; + screenReconnectDebounce.restart(); + } + } + } + Timer { id: surfaceResumeRecoveryTimer interval: 800 @@ -1003,6 +1051,14 @@ Item { osdResumeRecreateTimer.interval = 400; osdResumeRecreateTimer.restart(); + // This path runs its own recovery directly, so drop any queued or + // in-flight screen-reconnect recovery to avoid a redundant pass once + // its cooldown expires. + screenReconnectDebounce.stop(); + screenReconnectCooldown.stop(); + root._screenRecoveryCooldown = false; + root._screenRecoveryPending = false; + root.triggerSurfaceRecovery("sessionResumed"); } }