Skip to content

steamcompmgr: return the nudged focus window to (0, 0)#2274

Open
KogasaPls wants to merge 1 commit into
ValveSoftware:masterfrom
KogasaPls:nudge-round-trip
Open

steamcompmgr: return the nudged focus window to (0, 0)#2274
KogasaPls wants to merge 1 commit into
ValveSoftware:masterfrom
KogasaPls:nudge-round-trip

Conversation

@KogasaPls

@KogasaPls KogasaPls commented Jul 21, 2026

Copy link
Copy Markdown

Fixes #2241.

Since the initial commit of steamcompmgr in 2013, newly focused windows are given a "nudge" to (1, 1) before being snapped back to (0, 0). (I assume the nudge exists to force a ConfigureNotify, so I haven't removed it.) a563226 removed the snap so that windows requesting a specific position keep it, which left every newly focused window permanently parked at (1, 1) unless it asked for something else. On Wine ≥ 11 this can set up a race where Wine repeatedly shifts the window between (0, 0) and (1, 1), visible as persistent flicker and a storm of ConfigureNotify events.

Fix

Move the window back to (0, 0) immediately after the nudge. This restores the original first-focus behavior and is strictly narrower than reverting a563226: windows that request a different position still keep it, so a563226's fix and the follow-ups built on non-zero origins (48a3be2, 23a53b9) survive.

With the nudge-back in place, the repro procedure from #2241 produced 5 ConfigureNotify events for the whole session (versus 3253 in under a minute without it) and no flicker.

Bug Mechanism

(traced with WINEDEBUG=trace+x11drv and an instrumented gamescope)

  1. A fullscreen game maximizes its screen-sized window before first map: a same-rect no-op that sets WS_MAXIMIZE without any X config request. At first focus gamescope nudges the window to (1, 1), and Wine adopts the WM-imposed position into its x11-side desired state and, via window_update_client_config, the win32-side rect. The window is now parked at (1, 1) while win32 believes it is maximized.

  2. The mismatch sits dormant until a window event makes Wine re-evaluate. Any window creation that runs gamescope's focus pass will do: in a real session, the 1x1 clipboard-owner window a Ctrl+X creates (in the recorded repro runs, Wine's own startup churn fires it within ~200 ms). The focus pass emits a ConfigureNotify for the game window; window_update_client_state derives WS_MAXIMIZE from _NET_WM_STATE_MAXIMIZED, which gamescope never sets, concludes the window is no longer maximized, and posts SC_RESTORE.

  3. The game re-maximizes in response, requesting (0, 0). Since it reacts from inside the restore, the restore's own transaction finishes last and submits its stale (1, 1) while the (0, 0) config is still in flight. window_needs_config_change_delay defers it because another config is pending (its trace message blames _NET_WM_STATE/_MOTIF_WM_HINTS, but the per-round hint rewrites change no decorations and are not the trigger). When the granted (0, 0)'s ConfigureNotify returns, window_configure_notify preserves the stale desired rect and immediately re-requests it ("preserving delayed config", bf0813e3).

  4. gamescope grants every request verbatim and issues no moves of its own after the one-time nudge, so the granted (1, 1) parks the window again and its ConfigureNotify wakes the next round. The WoW session trace logged 1623 requests for (1, 1) and 1623 for (0, 0), exactly paired; the minimal repro cycles in roughly 200 µs per round.

Wine has a guard against exactly this kind of position fight (see the comment in window_update_client_config), but it requires the WM-imposed rect to fully cover the monitor, which is false at (1, 1). The same classification is why a single round dies out on its own: once the window truly rests at (0, 0), no further restore can fire. The loop persists only while each round ends back at (1, 1).

Wine 10.0 converges after one exchange: an unexpected ConfigureNotify is simply adopted into the desired state ("avoid requesting the same state again", the same adoption that absorbs the nudge in step 1). The 10.9 commit bf0813e3 (winex11: Serialize managed window config change requests) made configs that arrive while another request is in flight be preserved and re-requested instead, which is what sustains the loop; Proton 10 (Wine 10.0 base) therefore shows a single-frame blip where Proton 11 loops indefinitely.

Follow-ups

  • This change should preserve the fixes introduced by a563226; it'd be great if someone could confirm with a test.
  • One related bug which has existed since 2013: a window that requests a non-(0, 0) position before its first focus and never re-asserts it ends up at (0, 0). This was the behavior prior to a563226, after which it would end up at (1, 1) instead, but it's essentially the same bug either way. I'm not aware of a game that does this, but it may be worth fixing while we're here.
  • The (1, 1) nudge's purpose is undocumented. It may be obsolete; removing it entirely would be simpler than nudging back.
  • Possibly worth reporting to Wine: a WM that imposes a position once and never re-asserts it shouldn't be able to cause an unbounded reconfigure loop, and Wine's own guard against this fight class can't engage for rects that don't fully cover a monitor.
  • Produce a minimal repro of the self-sustaining reconfiguration loop I observed in World of Warcraft. Repro posted below.

a563226 removed the forced snap back to (0, 0), leaving the one-shot
nudge parked at (1, 1) permanently. Wine records that as the WM-imposed
position, and on Wine >= 11 its position reconciliation fights the stale
(1, 1) endlessly once woken (e.g. by a clipboard cut), presenting as
persistent black flickering. End the nudge back at (0, 0); windows that
request a different position still keep it.

Fixes: ValveSoftware#2241
@KogasaPls

Copy link
Copy Markdown
Author

I had Claude Fable 5 attempt to put together a minimal reproduction based on my understanding of the bug. As far as I can tell it mostly validates my story, and validates the fix, but take it with a pile of salt of course.

https://github.com/KogasaPls/gamescope-2241-repro

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.

Black flickering in World of Warcraft

1 participant