Window stops presenting for seconds under sustained keyboard input (WM_PAINT starvation; dispatch_key_event draws without presenting)
Ran into this hang bug while working on https://github.com/ProjectHax/muxel -- keyboard repeats hang. Traced a fix, see AI analysis below. Let me know if I can help with any more data.
Summary
On Windows, a gpui window can stop presenting frames entirely for 5–15 seconds while the user holds or rapidly types keys, then catch up the instant input stops. Rendering continues the whole time — element paint() runs at full rate — but no Present() call reaches DXGI, so the glass freezes. We hit this in a gpui-based terminal multiplexer (multiple PTY entities notifying while the user types); I believe #50883 ("terminal freezes / extreme input lag when holding keys") is the same bug observed from the symptom side.
Mechanism (verified on main, 2026-07-22)
Two pieces interact:
Presents only happen from
WM_PAINT. The vsync thread callsRedrawWindow(hwnd, None, None, RDW_INVALIDATE)each tick (crates/gpui_windows/src/platform.rs:349).RDW_INVALIDATEmarks the region; Windows synthesizes the actualWM_PAINTonly when the thread's message queue is otherwise idle — it is the lowest-priority message.dispatch_key_eventdraws without presenting.crates/gpui/src/window.rs(dispatch_key_event):if self.invalidator.is_dirty() { self.draw(cx).clear(cx); }This refreshes the dispatch tree, consumes the dirty flag, and never presents. The only
present()calls live in theon_request_framecallback — which only runs fromWM_PAINT.
Under sustained keyboard input plus background entity notifies (in our case: PTY output at ~20 chunks/s across several terminal entities, each notify posted to the UI thread), the message queue never goes idle, so WM_PAINT is never synthesized. Every notify dirties the window; the next key event's dispatch draws it and clears the flag; the frame is complete and never shown. Release the key → queue drains → WM_PAINT fires → present → the window "catches up" instantly.
Evidence
- PresentMon trace: during a 15s key-hold, zero
Present()calls for 15.05s (Composed: Flipswapchain), while our in-process counters show element paints running at ~20/s in every 500ms interval of the same window (timestamps aligned). Presents resume within ~1 frame of key release. Trace available — I can re-capture and share on request. - A per-paint color-cycling quad painted by our element froze on glass during the hold — frames were being rendered, not displayed.
- Severity scales with background notify traffic: with 7 terminal entities visible the freeze starts after ~5 held keys; with 3 visible it takes ~30.
- Not GPU/compositor-side: reproduces identically on two GPUs (NVIDIA dGPU and Intel iGPU displays), no device-lost or render errors logged, event log clean, and forcing DWM compositions (mouse wiggle in another window) does not unfreeze it.
Repro sketch
Any gpui app on Windows where entities cx.notify() at ~10–20Hz while the user holds a key should show it. Our app is a terminal multiplexer (PTY reader threads → async channel → entity notify per batch); holding any printable key in a busy workspace freezes the focused window's pixels while the app runs normally underneath. I can share a minimal repro if useful (timer-driven notifying entity + key repeat).
Workaround we ship
A watchdog thread calling RedrawWindow(hwnd, None, None, RDW_INVALIDATE | RDW_UPDATENOW) on the UI thread's windows every 8ms. RDW_UPDATENOW delivers WM_PAINT through the sent-message channel, which bypasses posted-queue priority, so a present opportunity always arrives. With nothing dirty the request-frame handler no-ops, so idle cost is small. This fully resolves the freeze for us.
Possible upstream fixes
- Present after the
dispatch_key_eventdraw when it actually drew (the frame is complete at that point), or - deliver the vsync thread's repaint via
RDW_UPDATENOW/sent message instead of relying on queue-idleWM_PAINTsynthesis, or - have input dispatch set
needs_presentand give presents a delivery path that isn't starved by input.
Related
- #50883 — same symptoms on Windows (terminal freeze while holding keys, catch-up on release); investigation stalled at an ETW trace. Likely this.
- #60295 (merged today) — touches the same
draw_window/re-entrancy path but addresses the arena crash, not present starvation. - #53822's
input-latency-histogramfeature should show this directly: worst-case input→present latency equals the hold duration.
Environment: Windows 11 (build 26200), gpui rev 1d217ee (via gpui-component) — mechanism re-verified against main today; the relevant code in dispatch_key_event and the vsync thread is unchanged.
Source: zed-industries/zed