fix(taskbar): repair work-area collapse & restore windows after display re-init (#3045 #4827 #2086)#5118
Open
popstas wants to merge 5 commits into
Open
fix(taskbar): repair work-area collapse & restore windows after display re-init (#3045 #4827 #2086)#5118popstas wants to merge 5 commits into
popstas wants to merge 5 commits into
Conversation
…play re-init (valinet#3045 valinet#4827 valinet#2086) After a display re-init (idle display-off + wake, sleep/resume, DP deep sleep, refresh-rate/resolution change) the Win10 / ep_taskbar desktop work area collapses and is never recomputed, and snapped windows the shell shoved into the transient collapsed band are left stranded. Restarting explorer was the only fix. Add a work-area watchdog on EP's service window: - Listens for display power/resume/mode events, debounces, and validates each monitor's work area against its taskbar band (per-monitor, EP-tray only, so third-party appbars are never touched). - On a detected collapse: soft-kick the taskbar recompute (WM_DISPLAYCHANGE), then escalate to SystemParametersInfoW(SPI_SETWORKAREA) for the broken monitors (the proven repair) with an anti-loop cooldown. - Snapshot top-level window rects on display-OFF/suspend (which precede the wake-time collapse); after a verified repair, restore any snapshotted window the shell displaced into the former collapsed band to its snapshot rect (position + size), with a conservative filter and a band anti-conflict guard so user-placed and maximized windows are left alone. - Everything is logged to %TEMP%\ep_workarea_watchdog.log for field diagnosis. Gated by a new "FixWorkAreaAfterDisplayReinit" setting (default on), exposed as a taskbar checkbox in the GUI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two avoidable delays in the watchdog repair chain: - The soft WM_DISPLAYCHANGE kick never heals the prebuilt ep_taskbar (OldTaskbar=2), yet attempt 1 always spent it before escalating - a wasted ~1.5s verify round-trip. Skip straight to the SPI repair for ep_taskbar; classic TrayUI still gets the soft kick first. - SPI_SETWORKAREA with SPIF_SENDCHANGE is synchronous, so the work area is already committed when it returns. Re-check sanity inline and restore the displaced windows right away instead of arming another ~1.5s verify timer. Measured on the test machine: window shift->restore dropped from ~7s to ~4.4s. The remaining latency is inherent (2s settle debounce + ~3.5s blocking SENDCHANGE broadcast). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SPI_SETWORKAREA repair used SPIF_SENDCHANGE, whose synchronous WM_SETTINGCHANGE broadcast to every top-level window blocked the service thread ~3.5s - the bulk of the remaining restore latency. Drop the flag: without it the work-area metric is still set synchronously (GetMonitorInfo reads the new value immediately), so we only lose the broadcast that reflows maximized windows. Handle that ourselves instead: - Snapped/displaced windows: restored from the snapshot as before. - Maximized windows on a repaired monitor: reflow with a targeted SetWindowPos to the repaired work area (inflated by the maximize frame overhang), instead of skipping them and relying on the broadcast. - A non-blocking PostMessage(HWND_BROADCAST, WM_SETTINGCHANGE) is posted as a courtesy so other work-area consumers refresh without stalling us; our own re-arm is suppressed by the existing repair cooldown. Measured on the test machine: SPI call ~3.5s -> ~3ms; snapped window shift->restore ~4.4s -> ~0.8s. Maximized reflow lands pixel-identical to the old SENDCHANGE result; window z-order preserved (SWP_NOZORDER). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stretch it Each display event (WM_DISPLAYCHANGE / WM_SETTINGCHANGE / tray stuck-place change) re-armed the 2000ms debounce from scratch. On a noisy wake the storm kept resetting it, so detection - and thus the window restore - fired only ~2000ms after the LAST event, stretching the displaced-window duration to ~3.3s in one measured cycle. Track the first arm of a cycle (g_epFirstArmTick) and clamp each re-arm's interval so the timer never fires later than MAX_DEBOUNCE (2500ms) after that first arm; once the cap is reached, stop extending and let the pending timer fire. Reset the tick at every cycle-completion point. Early re-arms still get the full 2000ms debounce (topology settling); only a prolonged storm is bounded. Measured: displaced-window duration went from a 0.76-3.35s spread down to a tight ~0.95-1.3s. Collapse still detected and repaired every cycle; no empty cycles. (Residual: WM_TIMER is low-priority and can still be briefly starved while the wake queue drains - the cap bounds timer expiry, not queue drain.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op leftovers Review-pass fixes and simplification, no functional change to the happy path: - tray thread now posts EP_WM_ARM_WORKAREA_WATCHDOG instead of calling EP_ArmWorkAreaWatchdog directly, so all watchdog/debounce/snapshot state is genuinely single-threaded (the old direct call mutated shared state unlocked) - restore pass: monitor identity check falls back to the monitor rect when the HMONITOR handle changed across the re-init (previously every window would be silently skipped), and runs before the maximized branch too - maximized reflow decides on current IsZoomed only (a window the user un-maximized since the snapshot is no longer blown up to a maximized rect) and computes the frame overhang with GetSystemMetricsForDpi for the window's own DPI instead of the system DPI - watchdog log is gated on the fix being active (no more perpetual log for Win11-taskbar users); empty log now means "fix disabled" - removed dead EP_AreWorkAreasSane, the unreachable snapshot overflow flag, a stale comment, duplicated WM_POWERBROADCAST branches, triple snapshot cleanup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Notes from me
Below text and code are from Fable:
Problem
After a display re-init (idle display-off + wake, sleep/resume, DP deep sleep, refresh-rate or resolution change) the Windows 10 /
ep_taskbartaskbar computes its stuck rect against a transient monitor geometry and never recomputes once the topology settles. Two user-visible symptoms:SPI_SETWORKAREAonly reflows maximized windows — snapped and floating windows are never moved back.Previously the only fix was restarting
explorer.exe. Refs #3045, #4827, #2086.What was done
A work-area watchdog hosted on EP's service window (
ExplorerPatcher/dllmain.c), plus a window snapshot/restore pass that undoes the collateral window displacement. Gated by a newFixWorkAreaAfterDisplayReinitsetting (default on), exposed as a taskbar checkbox in the EP GUI. Everything it does is logged to%TEMP%\ep_workarea_watchdog.logfor field diagnosis (log is written only while the fix is active; an empty log means the fix is disabled).Detection
WM_POWERBROADCAST(resume +CONSOLE_DISPLAY_STATEdisplay-ON viaRegisterPowerSettingNotification),WM_DISPLAYCHANGE,WM_SETTINGCHANGE(SPI_SETWORKAREA)on the service window, and the tray subclass'sWM_DISPLAYCHANGE/ stuck-place handlers (the tray receives these more reliably than a plain top-level window).DISPLAYCHANGE/SETTINGCHANGE) can no longer keep resetting the debounce and stretch the latency.Repair
WM_DISPLAYCHANGEposted to the tray windows — the same recompute an explorer restart triggers). The prebuiltep_taskbarignores the kick, so for it the watchdog goes straight toSPI_SETWORKAREAfor each broken monitor.SPI_SETWORKAREAis called withoutSPIF_SENDCHANGE: the synchronous broadcast blocked ~3.5 s waiting on every top-level window; without it the metric is still set synchronously (~3 ms). Displaced windows are reflowed by the restore pass instead, and a non-blocking courtesyPostMessage(HWND_BROADCAST, WM_SETTINGCHANGE)lets other apps refresh their cached work area.Window restore
SetWindowPos(frame overhang computed per-window-DPI).Threading model
All watchdog/snapshot/restore state lives on the service-window thread only (
WM_POWERBROADCAST,WM_TIMER,EP_WM_ARM_WORKAREA_WATCHDOG). The tray thread never touches it — it only postsEP_WM_ARM_WORKAREA_WATCHDOG. No locks needed.Edge cases handled
IsZoomed— a window the user restored right after wake is not blown up to a maximized rect.GetSystemMetricsForDpi(…, GetDpiForWindow(hwnd)), correct on monitors whose scale differs from the system DPI (verified pixel-exact on a 100% monitor under a 125% system DPI).WM_DISPLAYCHANGE(suppressed while a repair cycle runs) and our own SPI repair + courtesy broadcast come back asWM_SETTINGCHANGE(suppressed by a 15 s repair cooldown, stamped before the broadcast is posted).SWP_ASYNCWINDOWPOSso a hung app can't block the service thread;SWP_NOZORDERpreserves z-order (verified with a 6-window stack); capacity caps on snapshot (64), broken monitors (8), tray monitors (16).Verification (live, 4-monitor machine, one monitor collapsing ~100% of wake cycles)
SPI_SETWORKAREA result=1, verified sane) on every cycle across all test sessions.unchangedin the final run).SPIF_SENDCHANGEused to produce, at the window's own DPI.HEADbuild after the refactor commit (installed binary hash-matched the build artifact).