mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-08 17:11:46 -10:00
gamescope: EA sign-in stays above the EA launcher's splash (#2037)
* gamescope: EA sign-in stays above the EA launcher's splash The EA app's Steam launcher shows the game's splash as a layered tool window, and the EA app, another process, opens its sign-in window topmost. gamescope took the splash as the sign-in's override window, so the splash was painted over the sign-in and became gamescope's keyboard focus window (Burnout Paradise Remastered). With patch 0126 a window from another process that is neither topmost nor transient for a topmost focus window is no longer an override candidate for it, as on Windows. map_win now also reads _WINE_HWND_EXSTYLE: gamescope only took it from a PropertyNotify, which it listens for from map on, so a style Wine set before mapping was never known. * gamescope: the topmost rule needs both windows' pids gamescope keeps a window's pid at -1 when XRes cannot name its client, so an unknown pid next to a known one is not evidence of two processes. Patch 0126 now applies the rule only when both pids are known. The patch carries function names in its hunk headers, and the test also checks that its map_win hunk reads _WINE_HWND_EXSTYLE into hasHwndStyleEx (a check of the patch text, not of a running gamescope).
This commit is contained in:
1 parent
b7b08a40e8
commit
951270bc8f
3 files changed
+160
No files matched your search
@@ -209,3 +209,19 @@ the connector's windows (`mangoapp_nudge_app_frame`). With `DROIDDECK_HUD_FRAMES
|
||||
sets `/run/droiddeck-hud/frames`), gamescope maps that file and writes the same commits of connector 0,
|
||||
whether or not a mangoapp runs, plus the focused pid, output size, refresh, Steam focus and engine name.
|
||||
The layout is in the comment above `droiddeck_hud_frames_map`; the app reads it in `hudframes.c`.
|
||||
|
||||
## 0126 steamcompmgr: a topmost focus window keeps other processes' windows beneath it
|
||||
|
||||
EA games on Steam start through the EA app. Its Steam launcher (`EASteamLauncher.exe`) shows the
|
||||
game's splash as a layered tool window, and the EA app (`EADesktop.exe`, another process) opens its
|
||||
sign-in window `WS_EX_TOPMOST`. gamescope took the splash as the sign-in's override window, so it was
|
||||
painted over most of the sign-in and became gamescope's keyboard focus window (Burnout Paradise
|
||||
Remastered, `gamescopectl focus_info`). On Windows a topmost window stays above every window that is neither topmost nor
|
||||
owned by it, so a window from another process that is neither is no longer an override candidate for
|
||||
a topmost focus window. Both windows' pids must be known; gamescope keeps -1 when XRes cannot name the
|
||||
client, which says nothing about the process. Same-process popups, topmost popups (WebView2 and CEF menus) and windows
|
||||
transient for the focus window are unchanged. gamescope read `_WINE_HWND_EXSTYLE` only from a
|
||||
PropertyNotify, and it listens for those from map on, so the style Wine sets before mapping a window
|
||||
was never known; `map_win` now reads it with the other properties, which the layered-launcher and
|
||||
Xalia rules rely on too. `tools/tests/test_gamescope_override_stacking.py` compiles the rule from
|
||||
this patch.
|
||||
@@ -0,0 +1,43 @@
|
||||
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
|
||||
--- a/src/steamcompmgr.cpp
|
||||
+++ b/src/steamcompmgr.cpp
|
||||
@@ -4220,6 +4220,21 @@ static bool is_same_app_override_decorat
|
||||
( candidate->hwndStyleEx & uDecorationStyle ) == uDecorationStyle;
|
||||
}
|
||||
|
||||
+// Windows keeps a topmost window above every window that is neither topmost nor owned by it,
|
||||
+// so another process's splash cannot cover a topmost sign-in (the EA app's, over its Steam launcher).
|
||||
+static bool is_stacked_below_focus( steamcompmgr_win_t *candidate, steamcompmgr_win_t *focus )
|
||||
+{
|
||||
+ if ( candidate->pid <= 0 || focus->pid <= 0 || candidate->pid == focus->pid ||
|
||||
+ !candidate->hasHwndStyleEx || !focus->hasHwndStyleEx )
|
||||
+ return false;
|
||||
+
|
||||
+ if ( candidate->type == steamcompmgr_win_type_t::XWAYLAND && focus->type == steamcompmgr_win_type_t::XWAYLAND &&
|
||||
+ candidate->xwayland().transientFor == focus->xwayland().id )
|
||||
+ return false;
|
||||
+
|
||||
+ return ( focus->hwndStyleEx & WS_EX_TOPMOST ) && !( candidate->hwndStyleEx & WS_EX_TOPMOST );
|
||||
+}
|
||||
+
|
||||
static bool is_good_override_candidate( steamcompmgr_win_t *override, steamcompmgr_win_t* focus )
|
||||
{
|
||||
// Some Chrome/Edge dropdowns (ie. FH5 xbox login) will automatically close themselves if you
|
||||
@@ -4235,6 +4250,9 @@ static bool is_good_override_candidate(
|
||||
if ( is_same_app_override_decoration( override, focus ) )
|
||||
return false;
|
||||
|
||||
+ if ( is_stacked_below_focus( override, focus ) )
|
||||
+ return false;
|
||||
+
|
||||
if ( override->pid != focus->pid && !windows_share_app( override, focus ) )
|
||||
return false;
|
||||
|
||||
@@ -5836,6 +5854,7 @@ map_win(xwayland_ctx_t* ctx, Window id,
|
||||
w->isSteamStreamingClient = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientAtom, 0);
|
||||
w->isSteamStreamingClientVideo = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientVideoAtom, 0);
|
||||
w->steamAppID = get_prop(ctx, w->xwayland().id, ctx->atoms.gameAtom, 0);
|
||||
+ w->hwndStyleEx = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyleEx, 0, &w->hasHwndStyleEx);
|
||||
|
||||
if ( steamMode == true )
|
||||
{
|
||||
@@ -0,0 +1,101 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PATCH = ROOT / "tools/gamescope/patches/0126-topmost-focus-outranks-other-process-override.patch"
|
||||
|
||||
|
||||
def patched_rule():
|
||||
added = [line[1:] for line in PATCH.read_text().splitlines() if line.startswith("+") and not line.startswith("+++")]
|
||||
text = "\n".join(added)
|
||||
start = text.index("static bool is_stacked_below_focus")
|
||||
return text[start:text.index("\n}\n", start) + 3]
|
||||
|
||||
|
||||
PREAMBLE = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
static constexpr uint32_t WS_EX_TOPMOST = 0x00000008u;
|
||||
static constexpr uint32_t WS_EX_TOOLWINDOW = 0x00000080u;
|
||||
static constexpr uint32_t WS_EX_LAYERED = 0x00080000u;
|
||||
|
||||
enum class steamcompmgr_win_type_t { XWAYLAND, XDG };
|
||||
struct xwayland_t { unsigned long id = 0; unsigned long transientFor = 0; };
|
||||
struct steamcompmgr_win_t {
|
||||
int pid = 0;
|
||||
bool hasHwndStyleEx = false;
|
||||
uint32_t hwndStyleEx = 0;
|
||||
steamcompmgr_win_type_t type = steamcompmgr_win_type_t::XWAYLAND;
|
||||
xwayland_t x;
|
||||
xwayland_t &xwayland() { return x; }
|
||||
};
|
||||
|
||||
static steamcompmgr_win_t win(int pid, unsigned long id, uint32_t exstyle) {
|
||||
steamcompmgr_win_t w;
|
||||
w.pid = pid;
|
||||
w.x.id = id;
|
||||
w.hasHwndStyleEx = true;
|
||||
w.hwndStyleEx = exstyle;
|
||||
return w;
|
||||
}
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
int main() {
|
||||
steamcompmgr_win_t signin = win(4407, 0x2c00008, WS_EX_TOPMOST);
|
||||
steamcompmgr_win_t splash = win(4055, 0x1c00005, WS_EX_LAYERED | WS_EX_TOOLWINDOW);
|
||||
assert(is_stacked_below_focus(&splash, &signin));
|
||||
|
||||
steamcompmgr_win_t dropdown = win(4407, 0x2c00010, WS_EX_TOOLWINDOW);
|
||||
assert(!is_stacked_below_focus(&dropdown, &signin));
|
||||
|
||||
steamcompmgr_win_t menu = win(5120, 0x3000004, WS_EX_TOPMOST | WS_EX_TOOLWINDOW);
|
||||
assert(!is_stacked_below_focus(&menu, &signin));
|
||||
|
||||
steamcompmgr_win_t game = win(4407, 0x2c00008, 0);
|
||||
assert(!is_stacked_below_focus(&splash, &game));
|
||||
|
||||
steamcompmgr_win_t owned = win(4055, 0x1c00007, 0);
|
||||
owned.x.transientFor = signin.x.id;
|
||||
assert(!is_stacked_below_focus(&owned, &signin));
|
||||
|
||||
steamcompmgr_win_t unknown = win(4055, 0x1c00009, 0);
|
||||
unknown.hasHwndStyleEx = false;
|
||||
assert(!is_stacked_below_focus(&unknown, &signin));
|
||||
|
||||
steamcompmgr_win_t no_pid = win(-1, 0x1c0000b, WS_EX_LAYERED | WS_EX_TOOLWINDOW);
|
||||
assert(!is_stacked_below_focus(&no_pid, &signin));
|
||||
steamcompmgr_win_t signin_no_pid = win(-1, 0x2c00008, WS_EX_TOPMOST);
|
||||
assert(!is_stacked_below_focus(&splash, &signin_no_pid));
|
||||
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("c++"), "needs a C++ compiler")
|
||||
class OverrideStackingTest(unittest.TestCase):
|
||||
def test_a_topmost_focus_keeps_other_process_windows_beneath_it(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
source = Path(tmp) / "stacking.cpp"
|
||||
binary = Path(tmp) / "stacking"
|
||||
source.write_text(PREAMBLE + patched_rule() + MAIN)
|
||||
subprocess.run(["c++", "-std=c++17", "-Wall", "-Werror", str(source), "-o", str(binary)], check=True)
|
||||
self.assertEqual(subprocess.run([str(binary)], capture_output=True, text=True, check=True).stdout.strip(), "ok")
|
||||
|
||||
def test_map_win_reads_the_wine_extended_style(self):
|
||||
hunks = PATCH.read_text().split("\n@@ ")
|
||||
map_win = next(hunk for hunk in hunks if "map_win(" in hunk.split("\n", 1)[0])
|
||||
added = [line[1:].strip() for line in map_win.splitlines() if line.startswith("+")]
|
||||
self.assertIn("w->hwndStyleEx = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyleEx, 0, &w->hasHwndStyleEx);", added)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user