Skip to content

Fix system-wide mouse lag and CPU usage from the edge-light overlay - #214

Closed
bruceroberts-mantel wants to merge 4 commits into
shanselman:masterfrom
bruceroberts-mantel:fix/edge-light-overlay-performance
Closed

bruceroberts-mantel wants to merge 4 commits into
shanselman:masterfrom
bruceroberts-mantel:fix/edge-light-overlay-performance

Conversation

@bruceroberts-mantel

Copy link
Copy Markdown
Contributor

Summary

  • Eliminated system-wide mouse/input lag caused by the overlay, most noticeable during
    color-temperature changes and while hovering the cursor near the glow
  • Replaced the overlay's rendering with a natively managed layered window per monitor,
    rendered off the main UI thread, substantially reducing CPU usage
  • The overlay now adapts automatically to monitor configuration changes (resolution
    changes, displays connected/disconnected) instead of requiring a restart
  • Restored and widened the soft blurred edge so it displays reliably across monitors
    with different DPI scaling and display characteristics
  • Added an "Applying..." indicator in Settings while a color-temperature change is
    still rendering

Why

The previous implementation relied on a WPF AllowsTransparency window, which forces a
full re-render and re-transfer of the entire monitor-sized surface on every visual
change, however small. Combined with a global low-level mouse hook sharing the same UI
thread, this caused input lag across the whole system (not just the app) during normal
interaction - most severely during color-temperature changes, which could stall mouse
movement system-wide for as long as the render took.

Test plan

  • Brightness, color-temperature, and toggle all behave correctly on primary and
    additional monitors
  • No mouse/input lag during any interaction, including color-temperature dragging
  • Monitor resolution changes and connect/disconnect are picked up without a restart
  • Blur/soft edge renders correctly across monitors with different DPI

🤖 Generated with Claude Code

secretorbau and others added 4 commits September 10, 2026 15:48
Throttle/coalesce the global mouse hook and settings-slider ValueChanged
events so rapid input no longer floods the UI thread with redundant
work. Cache the blurred frame's rendered bitmap and reduce the drop
shadow blur radius to cut per-update rendering cost. Add temporary
perf-log instrumentation used to diagnose the sluggishness.

Confirmed working and responsive; still noticeably CPU-heavy under the
current AllowsTransparency-based rendering model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The overlay's AllowsTransparency WPF window forced a full re-render and
re-transfer of its entire monitor-sized surface on every visual change,
however small - brightness, color temperature, and cursor-hover updates
all paid that cost. Replace it with NativeLayeredWindow, a raw Win32
layered window managed independently of WPF, fed by GlowBitmapRenderer
(off-screen WPF rendering, cached as raw pixel bytes).

Brightness now uses UpdateLayeredWindow's own alpha-blend parameter -
no re-render at all. Hover updates punch the cursor hole directly into
the cached pixel bytes instead of re-running WPF's render pipeline
(which measured 100-300ms per call depending on monitor DPI), so moving
the cursor near the frame no longer pins a CPU core. The main window
itself shrinks to an inert 1x1px shell (hotkeys/mouse hook/DPI tracking
only) since a monitor-sized WPF window - even with empty content - was
independently making cursor movement sluggish across that whole screen.

Also remove the temporary PerfLog instrumentation used to diagnose this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Color-temperature changes (and resize/monitor-switch/toggle) were
blocking the main UI thread during WPF's off-screen render, which also
owns the global mouse hook - Windows expects that hook to return
promptly, so the block was showing up as system-wide mouse lag, not
just an unresponsive app. GlowRenderWorker moves that render onto a
dedicated thread with its own WPF Dispatcher; results are applied back
on the UI thread once ready, with a request-id check so a stale
in-flight render can't clobber a newer one. The Settings window shows
an "Applying..." indicator next to the color-temperature slider while
a change is still rendering, since it's no longer instant.

Also:
- Listen for Microsoft.Win32.SystemEvents.DisplaySettingsChanged and
  re-run monitor layout/geometry on any OS-level display change
  (monitor added/removed, resolution changed) - previously only this
  window's own size/position/DPI changes triggered a re-layout, which
  stopped happening once MainWindow became a fixed-size shell.
- Widen BlurRadius (76 -> 140) and MarginInset (40 -> 70) so the soft
  edge spans more physical pixels. Verified via direct alpha-channel
  sampling that the previous radius produced a smooth gradient in the
  rendered bitmap but only ~20px wide on lower-DPI monitors - narrow
  enough that some display paths (adapter/monitor color processing)
  were crushing it into a hard edge despite the source data being
  correct on both ends.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@shanselman

Copy link
Copy Markdown
Owner

Automated review follow-up from Copilot, acting on Scott's behalf.

Thanks for digging into this. There is a real hot path in the current design: mouse movement is posted through the WPF dispatcher while a monitor-sized transparent window is being updated, and moving toward explicitly managed layered windows is a promising direction. The working-set improvement in particular is substantial.

I am not comfortable merging the current version yet, because focused review and local ARM64 measurements found two blocking correctness/performance issues:

  1. An in-flight render can recreate native resources after its monitor context/window has been disposed. I reproduced this directly: calling the render path after disposal recreated a memory DC and bitmap, leaving GDI objects and a monitor-sized allocation alive. Removing an additional-monitor window does not currently invalidate its render generation, so a render completing after monitor removal can hit this path. Please cancel/invalidate each context before disposal, make every NativeLayeredWindow operation disposed-safe, and add a repeated monitor/toggle lifecycle test that verifies GDI counts remain stable.

  2. The low-level mouse-hook/UI thread still performs full-monitor surface work. The hover path copies and republishes the full monitor surface on the same UI thread that services the global mouse hook. The architecture makes system-wide lag plausible, but I could not reproduce a clear latency improvement over master in this environment.

The native ARM64 measurements on a 2180×1405 working area were:

Scenario master CPU PR CPU master peak WS PR peak WS
Edge hover ~60 Hz 23.0% 22.5% 711 MB 369 MB
Brightness ~60 Hz 11.7% 13.9% 701 MB 369 MB
Color temperature ~30 Hz 13.5% 87.6% 697 MB 520 MB

So the memory reduction is real and valuable, but color-temperature dragging currently consumes roughly 6.5× the CPU of master. A single native ARM64 color render took about 18–27 ms, which keeps the worker nearly saturated behind the 33 ms throttle. Please use trailing-edge debounce or a cheaper recolor path rather than repeatedly running the full WPF blur while dragging.

Before another review, could you please:

  • invalidate/cancel renders before disposing contexts and prove repeated toggle/display removal does not leak GDI resources;
  • move WH_MOUSE_LL handling onto a dedicated message-loop thread, or provide repeatable 4K/8K x64 and ARM64 traces showing hook latency remains safe while hover updates run;
  • address the color-temperature drag regression and provide before/after CPU measurements for x64 and ARM64, including multi-monitor if available;
  • preserve the existing z-order instead of raising the glow with HWND_TOPMOST, and clear hover state when the light is toggled back on;
  • generation-match cached pixels to window geometry so stale-sized surfaces cannot be applied after display changes.

This PR also combines the renderer rewrite with a much larger blur/look change, display hotplug behavior, and Settings UX changes. Please separate the visual/blur and display-management changes from the renderer work—or at minimum split them into independently reviewable commits—so we can evaluate the performance fix without unrelated visual behavior moving at the same time.

The Release build succeeds cleanly, and the overall direction is worth continuing. Thank you for taking on a difficult area and for the significant memory improvement; the remaining request is to make the lifecycle safe and demonstrate that the CPU/input path improves rather than moving the cost elsewhere.

shanselman added a commit that referenced this pull request Oct 29, 2026
Replace the WH_MOUSE_LL hook with a 33 ms GetCursorPos poll that only runs
while the light is on, so other apps' mouse input never waits on our UI thread.
Cache the blurred glow (BitmapCache) and apply brightness and the hover hole
outside the cache, so they no longer re-run the full-screen blur.

Maintainer patch based on the investigation in #214.

Co-authored-by: bruceroberts-mantel <269349613+bruceroberts-mantel@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@shanselman

Copy link
Copy Markdown
Owner

Hi @bruceroberts-mantel! I'm a Copilot triage agent helping maintain this repo (not Scott himself).

Thank you for digging into this. You correctly found that the global mouse hook and the overlay redraws were behind the lag and CPU use, and that investigation was really helpful.

We ended up landing a smaller maintainer patch in #220 that takes a different route. It removes the low-level mouse hook entirely and polls the cursor only while the light is on. It also caches the blurred glow so brightness and the cursor hole don't re-run the blur. That kept the change small and avoided some side effects we hit while testing this branch, like a GDI leak and a slowdown during color changes. You're credited as a co-author on that commit.

I'm leaving this PR open so you can decide what to do with it. If anything from your approach still seems worth adding, a follow-up would be welcome. Thanks again!

@shanselman

Copy link
Copy Markdown
Owner

Thanks for the thoughtful investigation and the substantial work on this. Your findings helped us identify the mouse-hook and rendering costs, and informed the smaller fix that shipped in #220. Since #220 addresses the reported issue with a simpler approach, this PR's implementation is no longer needed, so I'm closing it. We really appreciate your contribution.

@shanselman shanselman closed this Nov 2, 2026
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.

3 participants