Files
DroidDeck/tools/tests/test_gamescope_override_stacking.py
K11MCH1 951270bc8f 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).
2026-11-03 07:41:24 -05:00

102 lines
3.6 KiB
Python

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()