Skip to content

windows: fix freeze on keyboard layout switch (Punto Switcher) - #4582

Merged
kchibisov merged 1 commit into
rust-windowing:masterfrom
chilango74:fix/upstream-layout-switch-freeze
Jun 14, 2026
Merged

kchibisov merged 1 commit into
rust-windowing:masterfrom
chilango74:fix/upstream-layout-switch-freeze

Conversation

@chilango74

@chilango74 chilango74 commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes a freeze that affects any winit application on Windows when the keyboard
layout is switched by tools such as Punto Switcher (a popular auto layout
switcher). The window stops responding until the process is killed.

This was originally found and fixed in Warp's winit fork
(warpdotdev/winit#15); this PR
ports the same change to upstream.

Root cause (from a minidump of the frozen process)

A full dump was taken with procdump on the frozen window (original, unpatched
code) and analyzed with minidump-stackwalk using symbols generated from the
local system DLLs (so x64 unwind/CFI is exact).

Reliable top of the main thread:

0  win32u.dll!NtUserMsgWaitForMultipleObjectsEx        (instruction pointer)
1  winit::...::event_loop::wait_for_messages_impl       (Found by: call frame info)
2  user32.dll!CallNextHookEx
3  user32.dll!...
  • The thread is blocked in MsgWaitForMultipleObjectsEx — winit's
    event-loop wait (wait_for_messages_impl) — re-entered during message
    dispatch.
  • pshook64.dll (Punto Switcher's global hook) is injected into the process
    and present on the stack (CallNextHookEx), i.e. it runs on our thread during
    dispatch.
  • No mutex / LAYOUT_CACHE / WaitOnAddress frames anywhere.

So this is a re-entrant Win32 message-loop wait mediated by the layout
switcher's hook, not a Rust mutex self-deadlock.

Fix

Handle WM_INPUTLANGCHANGE to refresh the cached keyboard layout, then defer to
DefWindowProc (kept, per the
Win32 docs,
so the message still propagates to first-level child windows).

WM_INPUTLANGCHANGE => {
    { let mut layouts = LAYOUT_CACHE.lock().unwrap(); layouts.get_current_layout(); }
    result = ProcResult::DefWindowProc(wparam);
}

Testing (Windows 11 + Punto Switcher)

Built the winit test app for x86_64-pc-windows-gnu and exercised real Punto
Switcher layout switches (English ↔ Russian, confirmed by alphabet changes in
the key log). Isolation variants:

Variant WM_INPUTLANGCHANGE handler Result
original (no handler) — freezes (dump captured)
refresh + update_modifiers + Value(1) (swallow) skips DefWindowProc no freeze
refresh + DefWindowProc (this PR) keeps DefWindowProc no freeze
refresh only + DefWindowProc, no update_modifiers keeps DefWindowProc no freeze

So the layout-cache refresh is the minimal change that stops the freeze;
swallowing the message and update_modifiers are not needed.

Known limitation: the operative change was isolated empirically, and we have
proven what it is not (a mutex deadlock), but the precise reason a cache
refresh prevents the re-entrant wait is not fully traced — Punto Switcher's hook
is closed-source, and the dump shows no layout-loading frames at the freeze
point.

Related issues

Closes #4584 (standalone bug report with the minidump evidence).

  • No way to handle keyboard layout change events #1907 — "No way to handle keyboard layout change events" (this PR handles the WM_INPUTLANGCHANGE message)

  • API for keyboard layouts #2678 — "API for keyboard layouts"

  • Tested on all platforms changed (Windows, the only platform changed)

  • Added an entry to the changelog module

  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior

  • Created or updated an example program if it would help users understand this functionality

Any winit app on Windows freezes when the keyboard layout is switched by
tools such as Punto Switcher. A minidump of the frozen process shows the
main thread blocked in MsgWaitForMultipleObjectsEx (winit's event-loop
wait), re-entered during message dispatch through the switcher's injected
global hook (pshook64.dll, via CallNextHookEx) — a re-entrant Win32
message-loop wait, not a mutex deadlock (no LAYOUT_CACHE frames present).

Handle WM_INPUTLANGCHANGE to refresh the cached keyboard layout, then
defer to DefWindowProc so the message still propagates to first-level
child windows as the Win32 docs require. The cache refresh is the minimal
change that stops the freeze (verified on Windows 11 by isolation
variants); update_modifiers and swallowing the message are not needed.
@chilango74
chilango74 force-pushed the fix/upstream-layout-switch-freeze branch from 925e680 to eb3fc10 Compare June 10, 2026 11:21
Comment thread winit-win32/src/event_loop.rs
@kchibisov
kchibisov merged commit fb78ecb into rust-windowing:master Jun 14, 2026
50 checks passed
IrosTheBeggar added a commit to IrosTheBeggar/mstream-terminal-player that referenced this pull request Oct 1, 2026
…ut-switch freeze, Alt chords, startup stages

The Windows 10 run of the spike (PR #41, second comment) found the
window inheriting the conhost glyph set when launched from a plain
console, a hang on every keyboard-layout switch, Ctrl+Alt+q typing a
bare q and quitting the player, and a first frame two to three seconds
after the window appeared. What a Mac can fix of that: the glyph tier
is a pinnable module static now, and the window pins it modern beside
the palette; the vendored winit carries Warp's 0.30 backport of
rust-windowing/winit#4582 (the WM_INPUTLANGCHANGE arm that refreshes the
layout cache before DefWindowProc), untestable here but compiled by
CI's Windows leg; a key with Alt held delivers only the characters the
OS composed and nothing else, so AltGr's @ and € type and Ctrl+Alt+q
does nothing; the stats lever records every startup stage, the font
discovery and the wgpu adapter and device move to threads started
before the window exists (the vendored builder gained `with_device`),
and the warm first frame here fell from about 180 ms to about 150 ms;
the quit paths all exit within 50 ms here, with a "way out" timing line
for the Windows retest of its 11 s Ctrl+Q; the dump line names the
last press and what it hit, for the queue drag that never starts there.
Paste keeps the joiners that are part of words and emoji; the instance
lock drops after the App.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[Windows] Event loop freezes when an injected global hook (Punto Switcher) re-enters the message-loop wait on keyboard layout switch

2 participants