fix: prevent window oscillation after display topology change#342
Merged
acsandmann merged 4 commits intoApr 27, 2026
Merged
Conversation
After a lid open/close cycle with an external monitor, windows could enter a ~30 Hz oscillation between two display layout trees. The root cause was a desync between the VirtualWorkspaceManager (VWM) and the per-space layout trees in sync_tiled_windows_for_app. When a window moves to a new display/space: - The VWM is updated eagerly (remove from old space, add to new) - The layout trees are updated lazily via WindowsOnScreenUpdated events Two bugs conspired to keep the window in both trees: 1. The loop that re-adds known VWM windows to `desired` did not check whether the VWM still assigned each window to *this* space. A window moved to space2 was still re-added to space1's `desired` list from the VWM snapshot. 2. The "AX login-screen guard" (if desired.is_empty() && total_tiled_count == 0) only checked has_windows_for_app, not whether those tree windows had actually been moved to another space. It skipped removal when it should have allowed it. Fix in engine.rs: - Loop: skip re-adding any window whose VWM assignment for this space is gone (workspace_for_window returns None). - Guard: additionally check if the windows still in the layout tree have been moved away in the VWM; if so, allow removal to proceed. Fix in window_discovery.rs (emit_layout_events): - Add a pre-pass that updates the VWM for all claimed windows before sending any per-space WindowsOnScreenUpdated events. This makes the engine fix order-independent: regardless of which space's event fires first, the VWM already reflects the final assignment.
# Conflicts: # src/actor/reactor/tests.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a bug where windows oscillate ~30 times/second between two display layout trees after a lid open/close cycle with an external monitor.
Root cause:
sync_tiled_windows_for_appleft windows in two space layout trees simultaneously after a cross-display move. Both spaces issued conflictingSetWindowFramecalls that fed back into each other indefinitely.Two bugs in
engine.rsconspired to keep the window in the source space's tree even after the VWM had moved it to the destination:Loop re-add bug: The loop that re-adds known VWM windows to
desireddid not check whether the VWM still assigned each window to this space. A moved window was re-added to the source tree from a stale VWM snapshot.Guard bug: The "AX login-screen guard" (
if desired.is_empty() && total_tiled_count == 0) only checkedhas_windows_for_app, not whether those tree windows had been moved to another space. It skipped removal when it should have allowed it.Fix (engine.rs):
workspace_for_windowreturnsNone).Fix (window_discovery.rs):
emit_layout_eventsthat updates the VWM for all claimed windows before sending any per-spaceWindowsOnScreenUpdatedevents. This makes the engine fix order-independent regardless of which space's event fires first.Tests:
window_removed_from_source_space_when_dest_claims_it_first— Case 1: destination space fires first (engine guard fix)window_removed_from_source_space_when_source_empty_event_fires_first— Case 2: source empty event fires first (loop fix + pre-pass)window_preserved_in_space_on_empty_discovery_without_cross_space_move— regression guard for login-screen / AX-failure scenariodiscovery_after_display_change_places_window_on_correct_display— end-to-end integration test through the fullWindowsDiscovered → emit_layout_eventspathTest plan
cargo testpasses (all tests including 4 new regression tests)