Skip to content

fix(taskbar): repair work-area collapse & restore windows after display re-init (#3045 #4827 #2086)#5118

Open
popstas wants to merge 5 commits into
valinet:masterfrom
popstas:fix/workarea-window-restore
Open

fix(taskbar): repair work-area collapse & restore windows after display re-init (#3045 #4827 #2086)#5118
popstas wants to merge 5 commits into
valinet:masterfrom
popstas:fix/workarea-window-restore

Conversation

@popstas

@popstas popstas commented Jul 18, 2026

Copy link
Copy Markdown

Notes from me

  • I have been running this PR on my own machine for a week — it triggers reliably every time.
  • The logging and the UI toggle are temporary; I can remove them before merging (about 150 lines for logging and UI).
  • The comments may be too verbose — I'm happy to trim them down.
  • This only affects users who have the Windows 10 taskbar enabled.

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_taskbar taskbar computes its stuck rect against a transient monitor geometry and never recomputes once the topology settles. Two user-visible symptoms:

  1. The desktop work area collapses (observed: work-area height 3318 → 1712 on a portrait monitor) and never recovers — maximized windows and snap layouts are squeezed into a fraction of the screen.
  2. Windows the shell reflowed into the transient collapsed band stay stranded even after the work area is fixed, because SPI_SETWORKAREA only 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 new FixWorkAreaAfterDisplayReinit setting (default on), exposed as a taskbar checkbox in the EP GUI. Everything it does is logged to %TEMP%\ep_workarea_watchdog.log for field diagnosis (log is written only while the fix is active; an empty log means the fix is disabled).

Detection

  • Arm sources: WM_POWERBROADCAST (resume + CONSOLE_DISPLAY_STATE display-ON via RegisterPowerSettingNotification), WM_DISPLAYCHANGE, WM_SETTINGCHANGE(SPI_SETWORKAREA) on the service window, and the tray subclass's WM_DISPLAYCHANGE / stuck-place handlers (the tray receives these more reliably than a plain top-level window).
  • Debounce 2000 ms so the topology settles, capped at 2500 ms after the first arm of a cycle — a wake event storm (dozens of DISPLAYCHANGE/SETTINGCHANGE) can no longer keep resetting the debounce and stretch the latency.
  • Validation is per monitor: expected work area = monitor minus the EP taskbar band (autohide-aware). Two bug signatures: work area smaller than expected by >25% of the monitor (collapse), or a visible taskbar band still overlapping the work area beyond tolerance (not recalculated).

Repair

  • Classic TrayUI first gets a soft kick (WM_DISPLAYCHANGE posted to the tray windows — the same recompute an explorer restart triggers). The prebuilt ep_taskbar ignores the kick, so for it the watchdog goes straight to SPI_SETWORKAREA for each broken monitor.
  • SPI_SETWORKAREA is called without SPIF_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 courtesy PostMessage(HWND_BROADCAST, WM_SETTINGCHANGE) lets other apps refresh their cached work area.
  • The repair result is re-checked inline (no extra verify-timer round-trip); up to 3 attempts, then give up until the next event.

Window restore

  • On display-OFF / suspend (which precede the wake-time collapse) the watchdog snapshots top-level window rects on EP-tray monitors — a reliable pre-damage baseline.
  • After a verified repair it restores position+size of exactly those windows that the shell displaced into the former collapsed band, and re-maximizes still-maximized windows to the repaired work area with a targeted 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 posts EP_WM_ARM_WORKAREA_WATCHDOG. No locks needed.

Edge cases handled

  • Third-party appbars: monitors without an EP taskbar are never validated, repaired, snapshotted, or restored — foreign work-area reservations are untouched.
  • Autohide taskbar: expected work area = full monitor; the overlap signature is skipped.
  • User-placed windows: a window is only restored if its current rect lies fully inside the former collapsed band (+32 px tolerance) and it actually moved since the snapshot. Negative-tested: a window manually placed inside the band stays put.
  • Stale snapshots: restore requires the snapshot to predate the last display-ON and the wake to be recent (90 s window) — a snapshot from yesterday can't yank today's layout.
  • HMONITOR handle churn: monitor matching falls back to the monitor rect when the handle changed across the re-init (both in the broken-record lookup and the per-window same-monitor check).
  • Un-maximized-by-user race: the maximize reflow keys off the window's current IsZoomed — a window the user restored right after wake is not blown up to a maximized rect.
  • Per-monitor DPI: the maximize frame overhang uses 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).
  • Feedback loops: our own soft kick bounces back as WM_DISPLAYCHANGE (suppressed while a repair cycle runs) and our own SPI repair + courtesy broadcast come back as WM_SETTINGCHANGE (suppressed by a 15 s repair cooldown, stamped before the broadcast is posted).
  • Robustness: windows that are gone/minimized/hidden or moved to another monitor are skipped; restores use SWP_ASYNCWINDOWPOS so a hung app can't block the service thread; SWP_NOZORDER preserves 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)

  • Collapse detected and repaired (SPI_SETWORKAREA result=1, verified sane) on every cycle across all test sessions.
  • Displaced snapped window restored to its exact baseline on every cycle; zero collateral moves on the three healthy monitors (32 of 34 snapshotted windows correctly skipped as unchanged in the final run).
  • Maximized window reflowed pixel-identical to what SPIF_SENDCHANGE used to produce, at the window's own DPI.
  • Latency: displaced-window duration cut from ~7 s to ~1–1.3 s; detect→restore is ~16 ms once the debounce fires.
  • Final run executed against the exact HEAD build after the refactor commit (installed binary hash-matched the build artifact).

popstas and others added 5 commits July 12, 2026 23:44
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant