Skip to content

alt-tab-per-monitor: rewrite v2.0.0 - state machine architecture#3465

Open
troshab wants to merge 1 commit intoramensoftware:mainfrom
troshab:alt-tab-per-monitor-v2
Open

alt-tab-per-monitor: rewrite v2.0.0 - state machine architecture#3465
troshab wants to merge 1 commit intoramensoftware:mainfrom
troshab:alt-tab-per-monitor-v2

Conversation

@troshab
Copy link
Copy Markdown

@troshab troshab commented Mar 4, 2026

Summary

Rewrites the alt-tab-per-monitor mod (by @L3r0yThingz) with proper state machine architecture, replacing the fragile timing-based approach.

Problems in v1.1.1

  • Timing hack: hardcoded kDeltaThreshold = 200ms - too short on slow systems, unnecessary on fast ones
  • 3 unsynchronized atomics + non-atomic g_CreateInstance_TickCount for state tracking
  • No Wh_ModUninit: hooks could fire during unload (undefined behavior)
  • Position/CreateFrame clear the Show flag: if never called (error path), flag stays set forever
  • 5 identical symbol hooks copy-pasted between Win10 and Win11 arrays
  • Wh_Log in IsViewVisible: called per-window, spams the log

Changes in v2.0.0

  • State machine (AltTabPhase enum: Inactive -> Creating -> PostCreate -> Showing -> Inactive)
  • RAII PhaseGuard for safe phase transitions (no leaked flags on error paths)
  • SRWLOCK synchronization instead of multiple atomics
  • Wh_ModUninit with HookCallCounterGuard for safe unloading
  • Configurable postCreateThresholdMs in mod settings (default 200ms)
  • Deduplicated 5 common symbol hooks into shared commonSymbolHooks[]
  • Removed per-window Wh_Log from IsViewVisible (was spamming log)
  • Position/CreateFrame no longer clear flags - PhaseGuard in Show handles cleanup

Compatibility

  • Windows 10 / Windows 11 / Windows 11 24H2 (Build 26100)
  • ExplorerPatcher compatibility preserved (optional Win10-style hooks on Win11)

Test plan

  • Alt+Tab shows only windows from cursor's monitor
  • Win+Alt+Tab shows all windows across all monitors
  • Mod enable/disable without crashes (Wh_ModUninit)
  • Settings hot reload (postCreateThresholdMs)
  • ExplorerPatcher compatibility (if installed)

Replace fragile timing hack with proper state machine architecture:

- State machine (AltTabPhase enum) instead of 3 unsynchronized atomics
- RAII PhaseGuard for safe phase transitions
- SRWLOCK synchronization for thread-safe state access
- Wh_ModUninit with hook call counter for safe unloading
- Configurable postCreateThresholdMs (was hardcoded 200ms)
- Deduplicated common symbol hooks between Win10/Win11
- Removed per-window Wh_Log spam from IsViewVisible
@m417z
Copy link
Copy Markdown
Member

m417z commented Mar 4, 2026

Thanks for the submission. Are you seeing an actual problem that your submission fixes?

@troshab
Copy link
Copy Markdown
Author

troshab commented Mar 4, 2026

Thanks for the submission. Are you seeing an actual problem that your submission fixes?

Yes, with the previous version of this module enabled, switching keys with Alt Shift became hell. If you clicked very quickly instead of holding Alt for a relatively long time, it did not switch the input language.

@m417z
Copy link
Copy Markdown
Member

m417z commented Mar 4, 2026

How is Alt+Shift related to Alt+Tab or to this mod? Please provide a way to reproduce the issue. And I'm sure it's possible to address it without rewriting the whole mod.

@troshab
Copy link
Copy Markdown
Author

troshab commented Mar 4, 2026

How is Alt+Shift related to Alt+Tab or to this mod? Please provide a way to reproduce the issue. And I'm sure it's possible to address it without rewriting the whole mod.

The connection is through CVirtualDesktop::IsViewVisible - this function is not exclusive to Alt+Tab. It's called by the taskbar, Win+Tab, and other shell components including the input switcher overlay. The mod's timing-based filter (kDeltaThreshold = 200ms) can leak into those calls because g_lastThreadIdForXamlAltTabViewHost_CreateInstance is never cleared - once set, any IsViewVisible call on that thread within 200ms gets filtered.

How I found it: I was debugging why Alt+Shift language switching was unreliable on Windows 11 24H2 (required holding keys instead of a quick press). After ruling out keyboard hooks (measured 0ms latency with a custom WH_KEYBOARD_LL monitor), I started disabling Windhawk mods one by one. Disabling alt-tab-per-monitor resolved the issue. Re-enabling it brought the problem back.

Reproduction: Enable the mod, then rapidly press Alt+Shift to switch input language. On systems with 2+ languages and multiple monitors, the switch is unreliable - sometimes it doesn't register, sometimes it double-switches. Disable the mod - the problem goes away.

As for the rewrite scope - beyond the Alt+Shift issue, the current code has a data race (g_CreateInstance_TickCount is a non-atomic ULONGLONG written and read from different contexts), no Wh_ModUninit (hooks can fire during unload), and Wh_Log in IsViewVisible that spams the log for every window. These issues are interconnected enough that a targeted fix would end up touching most of the same code. But I'm happy to split this into smaller PRs if preferred.

@m417z
Copy link
Copy Markdown
Member

m417z commented Mar 8, 2026

The mod's timing-based filter (kDeltaThreshold = 200ms) can leak into those calls because g_lastThreadIdForXamlAltTabViewHost_CreateInstance is never cleared - once set, any IsViewVisible call on that thread within 200ms gets filtered.

Right, but it can cause issues only up to 200ms after g_CreateInstance_TickCount is set. It doesn't seem to be the case in your reproduction.

Reproduction: Enable the mod, then rapidly press Alt+Shift to switch input language. On systems with 2+ languages and multiple monitors, the switch is unreliable - sometimes it doesn't register, sometimes it double-switches. Disable the mod - the problem goes away.

I can't reproduce it. I also have nothing logged when switching between languages with Alt+Shift. Can you post logs and a video recording? Perhaps you have some additional UI for one of the languages.

the current code has a data race (g_CreateInstance_TickCount is a non-atomic ULONGLONG written and read from different contexts)

It can be changed to std::atomic<ULONGLONG>, although it's unlikely to be an issue in practice.

no Wh_ModUninit (hooks can fire during unload)

It applies for many other mods, and implementing a refcount in each case is a hassle. Note that:

  • It's quite unlikely to have one of the hooks running exactly at the time the mod is unloaded
  • The refcount solution helps but isn't perfect, since it's possible that one of the hooks is just about to be called
  • Latest versions of Windhawk do something similar automatically - Windhawk doesn't unload the mod until all hooks are returned

Wh_Log in IsViewVisible that spams the log for every window

That's a matter of preference. It has no impact when logging is disabled. Surely it doesn't warrant a refactor for the whole mod.

I'm happy to split this into smaller PRs if preferred.

Let's focus on understanding why the mod broke Alt+Shift for you, and fix that specifically.

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.

2 participants