#61469·zed

Window stops presenting for seconds under sustained keyboard input (WM_PAINT starvation; dispatch_key_event draws without presenting)

Author: railapexCreated Jul 22, 2026Updated Sep 17, 2026
Labelsplatform:windowsarea:gpui.contrib/zed team onlyreach:some usersseverity:S3

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:

  1. Presents only happen from WM_PAINT. The vsync thread calls RedrawWindow(hwnd, None, None, RDW_INVALIDATE) each tick (crates/gpui_windows/src/platform.rs:349). RDW_INVALIDATE marks the region; Windows synthesizes the actual WM_PAINT only when the thread's message queue is otherwise idle — it is the lowest-priority message.

  2. dispatch_key_event draws 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 the on_request_frame callback — which only runs from WM_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: Flip swapchain), 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_event draw 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-idle WM_PAINT synthesis, or
  • have input dispatch set needs_present and 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-histogram feature 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.