Skip to content

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

Open
chilango74 wants to merge 1 commit into
rust-windowing:masterfrom
chilango74:fix/upstream-layout-switch-freeze
Open

windows: fix freeze on keyboard layout switch (Punto Switcher)#4582
chilango74 wants to merge 1 commit into
rust-windowing:masterfrom
chilango74:fix/upstream-layout-switch-freeze

Conversation

@chilango74
Copy link
Copy Markdown

@chilango74 chilango74 commented May 31, 2026

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.

  • 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.
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.

1 participant