|
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
|
|
import argparse
|
|
|
|
|
import ctypes
|
|
|
|
|
import ctypes.util
|
|
|
|
|
import math
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import select
|
|
|
|
|
@@ -301,24 +302,40 @@ class _X11:
|
|
|
|
|
self._restore_error_handler()
|
|
|
|
|
|
|
|
|
|
def root_window_ids(self) -> set[int]:
|
|
|
|
|
return set(self._root_windows())
|
|
|
|
|
# Keep client IDs as well as frames: decorations can be reparented while
|
|
|
|
|
# another account starts, and a new frame must not make an old game new.
|
|
|
|
|
frames = self._root_windows()
|
|
|
|
|
return set(frames) | {self._client_window(frame) for frame in frames}
|
|
|
|
|
|
|
|
|
|
def find_first_window(
|
|
|
|
|
self,
|
|
|
|
|
title_fragment: str,
|
|
|
|
|
*,
|
|
|
|
|
excluded_window_ids: Collection[int],
|
|
|
|
|
expected_pid: int | None = None,
|
|
|
|
|
) -> tuple[int, str] | None:
|
|
|
|
|
windows: list[tuple[int, str]] = []
|
|
|
|
|
for window in self._root_windows():
|
|
|
|
|
for frame in self._root_windows():
|
|
|
|
|
if frame in excluded_window_ids:
|
|
|
|
|
continue
|
|
|
|
|
window = self._client_window(frame)
|
|
|
|
|
if window in excluded_window_ids:
|
|
|
|
|
continue
|
|
|
|
|
if expected_pid is not None and self.window_pid(window) != expected_pid:
|
|
|
|
|
continue
|
|
|
|
|
# Reparenting WMs need not copy a client's title onto its outer frame.
|
|
|
|
|
title = self._window_title(window)
|
|
|
|
|
if title is None and frame != window:
|
|
|
|
|
title = self._window_title(frame)
|
|
|
|
|
if title is not None:
|
|
|
|
|
windows.append((window, title))
|
|
|
|
|
selected = _select_game_window(windows, title_fragment, excluded_window_ids)
|
|
|
|
|
if selected is None:
|
|
|
|
|
return None
|
|
|
|
|
window, title = selected
|
|
|
|
|
return self._client_window(window), title
|
|
|
|
|
return _select_game_window(windows, title_fragment, excluded_window_ids)
|
|
|
|
|
|
|
|
|
|
def window_pid(self, window: int) -> int | None:
|
|
|
|
|
"""Read the client's process identity within this X server."""
|
|
|
|
|
|
|
|
|
|
values = self._long_property(window, b"_NET_WM_PID", 1)
|
|
|
|
|
return values[0] if values and values[0] > 0 else None
|
|
|
|
|
|
|
|
|
|
def find_window_by_pid(self, expected_pid: int) -> int | None:
|
|
|
|
|
"""Find a top-level host window created by one native process."""
|
|
|
|
|
@@ -1057,6 +1074,7 @@ class _X11:
|
|
|
|
|
window: int,
|
|
|
|
|
*,
|
|
|
|
|
borderless: bool,
|
|
|
|
|
raise_window: bool = True,
|
|
|
|
|
) -> tuple[int, int] | None:
|
|
|
|
|
# Capture the real client size for both decorated and borderless windows.
|
|
|
|
|
# Wine/DXVK can otherwise retain a desktop-sized render surface inside a
|
|
|
|
|
@@ -1064,7 +1082,8 @@ class _X11:
|
|
|
|
|
target_size = self.window_size(window)
|
|
|
|
|
if borderless:
|
|
|
|
|
self._remove_decorations(window)
|
|
|
|
|
self.library.XMapRaised(self.display, window)
|
|
|
|
|
if raise_window:
|
|
|
|
|
self.library.XMapRaised(self.display, window)
|
|
|
|
|
self.library.XFlush(self.display)
|
|
|
|
|
return target_size
|
|
|
|
|
|
|
|
|
|
@@ -1133,6 +1152,8 @@ class _X11:
|
|
|
|
|
self.library.XSync(self.display, 0)
|
|
|
|
|
|
|
|
|
|
def window_is_focused(self, window: int) -> bool:
|
|
|
|
|
"""Accept focus on the client or one of Wine's child input windows."""
|
|
|
|
|
|
|
|
|
|
focused = ctypes.c_ulong()
|
|
|
|
|
revert_to = ctypes.c_int()
|
|
|
|
|
if self.library.XGetInputFocus(
|
|
|
|
|
@@ -1141,7 +1162,33 @@ class _X11:
|
|
|
|
|
ctypes.byref(revert_to),
|
|
|
|
|
) != 1:
|
|
|
|
|
return False
|
|
|
|
|
return int(focused.value) == window
|
|
|
|
|
candidate = int(focused.value)
|
|
|
|
|
visited: set[int] = set()
|
|
|
|
|
# None (0) and PointerRoot (1) are special focus values, not clients.
|
|
|
|
|
while candidate not in {0, 1} and candidate not in visited and len(visited) < 64:
|
|
|
|
|
if candidate == window:
|
|
|
|
|
return True
|
|
|
|
|
visited.add(candidate)
|
|
|
|
|
root = ctypes.c_ulong()
|
|
|
|
|
parent = ctypes.c_ulong()
|
|
|
|
|
children = ctypes.POINTER(ctypes.c_ulong)()
|
|
|
|
|
child_count = ctypes.c_uint()
|
|
|
|
|
try:
|
|
|
|
|
status = self.library.XQueryTree(
|
|
|
|
|
self.display,
|
|
|
|
|
candidate,
|
|
|
|
|
ctypes.byref(root),
|
|
|
|
|
ctypes.byref(parent),
|
|
|
|
|
ctypes.byref(children),
|
|
|
|
|
ctypes.byref(child_count),
|
|
|
|
|
)
|
|
|
|
|
if status == 0 or candidate == root.value:
|
|
|
|
|
return False
|
|
|
|
|
candidate = int(parent.value)
|
|
|
|
|
finally:
|
|
|
|
|
if children:
|
|
|
|
|
self.library.XFree(ctypes.cast(children, ctypes.c_void_p))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def window_is_active(self, window: int) -> bool:
|
|
|
|
|
"""Return whether the desktop already considers this client active."""
|
|
|
|
|
@@ -1167,10 +1214,22 @@ class _X11:
|
|
|
|
|
)
|
|
|
|
|
if active_window == 0:
|
|
|
|
|
return False
|
|
|
|
|
# Wine may keep its last input timestamp on a separate user-time window.
|
|
|
|
|
# Use this client's timestamp; borrowing another application's timestamp
|
|
|
|
|
# would incorrectly bypass the desktop's focus-stealing prevention.
|
|
|
|
|
user_time_window = self._long_property(window, b"_NET_WM_USER_TIME_WINDOW", 1)
|
|
|
|
|
time_window = user_time_window[0] if user_time_window and user_time_window[0] else window
|
|
|
|
|
user_time = self._long_property(
|
|
|
|
|
time_window,
|
|
|
|
|
b"_NET_WM_USER_TIME",
|
|
|
|
|
1,
|
|
|
|
|
)
|
|
|
|
|
if not user_time and time_window != window:
|
|
|
|
|
user_time = self._long_property(window, b"_NET_WM_USER_TIME", 1)
|
|
|
|
|
return self._send_root_client_message(
|
|
|
|
|
window,
|
|
|
|
|
active_window,
|
|
|
|
|
(1, _X_CURRENT_TIME, 0, 0, 0),
|
|
|
|
|
(1, user_time[0] if user_time else _X_CURRENT_TIME, 0, 0, 0),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _send_root_client_message(
|
|
|
|
|
@@ -1495,7 +1554,14 @@ def _prepare_game_window(
|
|
|
|
|
*,
|
|
|
|
|
borderless: bool,
|
|
|
|
|
suppress_wm_ping: bool,
|
|
|
|
|
raise_window: bool = True,
|
|
|
|
|
) -> None:
|
|
|
|
|
if not raise_window:
|
|
|
|
|
try:
|
|
|
|
|
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
|
|
|
|
return
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
|
|
|
|
if suppress_wm_ping:
|
|
|
|
|
try:
|
|
|
|
|
ping_suppressed = mapper.suppress_wm_ping(window)
|
|
|
|
|
@@ -1520,7 +1586,11 @@ def _prepare_game_window(
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
target_size = mapper.map_window(window, borderless=borderless)
|
|
|
|
|
target_size = mapper.map_window(
|
|
|
|
|
window,
|
|
|
|
|
borderless=borderless,
|
|
|
|
|
**({"raise_window": False} if not raise_window else {}),
|
|
|
|
|
)
|
|
|
|
|
forced_windowed = False
|
|
|
|
|
try:
|
|
|
|
|
forced_windowed = mapper.force_windowed_state(window)
|
|
|
|
|
@@ -1596,18 +1666,67 @@ def _apply_game_window_identity(mapper: _X11, window: int) -> None:
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _focus_game_window(mapper: _X11, window: int) -> None:
|
|
|
|
|
def _focus_game_window(
|
|
|
|
|
mapper: _X11,
|
|
|
|
|
window: int,
|
|
|
|
|
*,
|
|
|
|
|
activate: bool = True,
|
|
|
|
|
diagnostics: WindowDiagnosticRecorder | None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Let the dashboard process its ready event before focusing the game."""
|
|
|
|
|
|
|
|
|
|
time.sleep(WINDOW_FOCUS_DELAY_SECONDS)
|
|
|
|
|
activation_requested = False
|
|
|
|
|
activation_submitted = False
|
|
|
|
|
outcome = "not_activated"
|
|
|
|
|
try:
|
|
|
|
|
mapper.focus_window(window)
|
|
|
|
|
if activate:
|
|
|
|
|
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
|
|
|
|
outcome = "not_mapped"
|
|
|
|
|
return
|
|
|
|
|
if not mapper.window_is_active(window):
|
|
|
|
|
activation_requested = True
|
|
|
|
|
activation_submitted = mapper.activate_window(window)
|
|
|
|
|
if not activation_submitted:
|
|
|
|
|
outcome = "request_failed"
|
|
|
|
|
return
|
|
|
|
|
# XSendEvent success only means the request was sent. The desktop
|
|
|
|
|
# may refuse it, or take longer than a fixed sleep to activate us.
|
|
|
|
|
deadline = time.monotonic() + WINDOW_STATE_CONFIRM_TIMEOUT_SECONDS
|
|
|
|
|
while True:
|
|
|
|
|
if mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
|
|
|
|
outcome = "not_mapped"
|
|
|
|
|
return
|
|
|
|
|
if mapper.window_is_active(window):
|
|
|
|
|
break
|
|
|
|
|
remaining = deadline - time.monotonic()
|
|
|
|
|
if remaining <= 0:
|
|
|
|
|
return
|
|
|
|
|
time.sleep(min(WINDOW_CONFIRM_POLL_SECONDS, remaining))
|
|
|
|
|
# Recheck ownership after waiting and preserve an already-focused
|
|
|
|
|
# Wine child. Never override a declined activation with XMapRaised.
|
|
|
|
|
focused = _repair_game_window_focus(mapper, window)
|
|
|
|
|
else:
|
|
|
|
|
# Host focus may have changed during the dashboard handoff delay.
|
|
|
|
|
focused = _repair_gamescope_game_window_focus(mapper, window)
|
|
|
|
|
outcome = "focused" if focused else "focus_not_owned"
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
outcome = "error"
|
|
|
|
|
print(
|
|
|
|
|
f"Warning: could not focus the game window ({exc}); continuing normally",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
if diagnostics is not None:
|
|
|
|
|
diagnostics.record(
|
|
|
|
|
"focus_handoff",
|
|
|
|
|
window_id=window,
|
|
|
|
|
activation_requested=activation_requested,
|
|
|
|
|
activation_submitted=activation_submitted,
|
|
|
|
|
outcome=outcome,
|
|
|
|
|
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _repair_game_window_focus(
|
|
|
|
|
@@ -1632,7 +1751,7 @@ def _repair_game_window_focus(
|
|
|
|
|
active=active,
|
|
|
|
|
focused=focused,
|
|
|
|
|
)
|
|
|
|
|
if state == _X_WM_STATE_ICONIC:
|
|
|
|
|
if state != _X_WM_STATE_NORMAL:
|
|
|
|
|
return False
|
|
|
|
|
if not active:
|
|
|
|
|
return False
|
|
|
|
|
@@ -1641,6 +1760,9 @@ def _repair_game_window_focus(
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
# Diagnostic/property reads above can outlast an Alt-Tab or minimize.
|
|
|
|
|
if mapper.window_state(window) != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
|
|
|
|
return False
|
|
|
|
|
mapper.restore_input_focus(window)
|
|
|
|
|
repaired = mapper.window_is_focused(window)
|
|
|
|
|
if diagnostics is not None:
|
|
|
|
|
@@ -1697,13 +1819,15 @@ def _repair_gamescope_game_window_focus(
|
|
|
|
|
wm_state=_wm_state_name(state),
|
|
|
|
|
focused=focused,
|
|
|
|
|
)
|
|
|
|
|
if state == _X_WM_STATE_ICONIC:
|
|
|
|
|
if state != _X_WM_STATE_NORMAL:
|
|
|
|
|
return False
|
|
|
|
|
if focused:
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
if not _gamescope_host_focus_is_allowed() or mapper.window_state(window) != _X_WM_STATE_NORMAL:
|
|
|
|
|
return False
|
|
|
|
|
mapper.restore_input_focus(window)
|
|
|
|
|
repaired = mapper.window_is_focused(window)
|
|
|
|
|
if diagnostics is not None:
|
|
|
|
|
@@ -1769,6 +1893,9 @@ def _gamescope_host_focus_status(
|
|
|
|
|
return {"allowed": False, "bridge": "unreadable"}
|
|
|
|
|
observed_at = time.monotonic() if now is None else now
|
|
|
|
|
age = observed_at - published_at
|
|
|
|
|
# float() accepts NaN/infinity; round() would terminate the supervisor.
|
|
|
|
|
if not math.isfinite(published_at) or not math.isfinite(age * 1000):
|
|
|
|
|
return {"allowed": False, "bridge": "invalid"}
|
|
|
|
|
age_ms = round(age * 1000)
|
|
|
|
|
if age < 0.0:
|
|
|
|
|
return {"allowed": False, "bridge": "future", "age_ms": age_ms}
|
|
|
|
|
@@ -1919,10 +2046,22 @@ def _maintain_game_window_scaling(
|
|
|
|
|
process: subprocess.Popen[bytes],
|
|
|
|
|
window: int,
|
|
|
|
|
*,
|
|
|
|
|
window_title: str = WINDOW_TITLE,
|
|
|
|
|
existing_window_ids: Collection[int] = (),
|
|
|
|
|
expected_pid: int | None = None,
|
|
|
|
|
diagnostics: WindowDiagnosticRecorder | None = None,
|
|
|
|
|
) -> int:
|
|
|
|
|
"""Keep host scaling hints across Wine maximize/restore transitions."""
|
|
|
|
|
|
|
|
|
|
def keep_focus_monitoring() -> int:
|
|
|
|
|
return _maintain_game_window_focus(
|
|
|
|
|
mapper, process, window,
|
|
|
|
|
window_title=window_title,
|
|
|
|
|
existing_window_ids=existing_window_ids,
|
|
|
|
|
expected_pid=expected_pid,
|
|
|
|
|
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
mapper.select_scaling_hint_events(window)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
@@ -1938,7 +2077,7 @@ def _maintain_game_window_scaling(
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
return keep_focus_monitoring()
|
|
|
|
|
|
|
|
|
|
scaling_enabled = (
|
|
|
|
|
_enable_game_window_scaling(mapper, window)
|
|
|
|
|
@@ -1950,7 +2089,7 @@ def _maintain_game_window_scaling(
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if not scaling_enabled:
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
return keep_focus_monitoring()
|
|
|
|
|
|
|
|
|
|
repair_failures = 0
|
|
|
|
|
next_scaling_repair_at = 0.0
|
|
|
|
|
@@ -1965,7 +2104,7 @@ def _maintain_game_window_scaling(
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
return keep_focus_monitoring()
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if now >= next_focus_repair_at:
|
|
|
|
|
if diagnostics is None:
|
|
|
|
|
@@ -2005,7 +2144,7 @@ def _maintain_game_window_scaling(
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
return keep_focus_monitoring()
|
|
|
|
|
else:
|
|
|
|
|
repair_failures = 0
|
|
|
|
|
if hints_changed and diagnostics is not None:
|
|
|
|
|
@@ -2018,34 +2157,100 @@ def _maintain_game_window_scaling(
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_direct_game_window(
|
|
|
|
|
mapper: _X11,
|
|
|
|
|
window: int,
|
|
|
|
|
*,
|
|
|
|
|
window_title: str,
|
|
|
|
|
excluded_window_ids: Collection[int],
|
|
|
|
|
expected_pid: int | None,
|
|
|
|
|
diagnostics: WindowDiagnosticRecorder | None = None,
|
|
|
|
|
) -> int | None:
|
|
|
|
|
"""Follow a replacement from the same game without restoring minimized clients."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
state = mapper.window_state(window)
|
|
|
|
|
if (
|
|
|
|
|
state is not None
|
|
|
|
|
and state != _X_WM_STATE_WITHDRAWN
|
|
|
|
|
and (expected_pid is None or mapper.window_pid(window) == expected_pid)
|
|
|
|
|
):
|
|
|
|
|
return window
|
|
|
|
|
# A shared host display can contain several accounts with identical
|
|
|
|
|
# titles. Without the original client's PID, a replacement is ambiguous.
|
|
|
|
|
if expected_pid is None:
|
|
|
|
|
return None
|
|
|
|
|
found = mapper.find_first_window(
|
|
|
|
|
window_title,
|
|
|
|
|
excluded_window_ids=set(excluded_window_ids) | {window},
|
|
|
|
|
expected_pid=expected_pid,
|
|
|
|
|
)
|
|
|
|
|
if found is None:
|
|
|
|
|
return None
|
|
|
|
|
replacement, title = found
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
_apply_game_window_identity(mapper, replacement)
|
|
|
|
|
if diagnostics is not None:
|
|
|
|
|
diagnostics.record(
|
|
|
|
|
"game_window_replaced",
|
|
|
|
|
previous_window_id=window,
|
|
|
|
|
window_id=replacement,
|
|
|
|
|
**_sample_window_diagnostic_details(diagnostics, mapper, replacement),
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
f"Game window changed; continuing focus monitoring: {title}",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return replacement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _maintain_game_window_focus(
|
|
|
|
|
mapper: _X11,
|
|
|
|
|
process: subprocess.Popen[bytes],
|
|
|
|
|
window: int,
|
|
|
|
|
*,
|
|
|
|
|
window_title: str = WINDOW_TITLE,
|
|
|
|
|
existing_window_ids: Collection[int] = (),
|
|
|
|
|
expected_pid: int | None = None,
|
|
|
|
|
diagnostics: WindowDiagnosticRecorder | None = None,
|
|
|
|
|
) -> int:
|
|
|
|
|
"""Keep the keyboard-focus repair while retaining Wine's fixed-size hints."""
|
|
|
|
|
"""Keep keyboard-focus repair without making further window-size changes."""
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
"Game window resizing is disabled to prevent repeated Direct3D "
|
|
|
|
|
"device resets.",
|
|
|
|
|
"Game window focus monitoring active.",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
if expected_pid is None:
|
|
|
|
|
try:
|
|
|
|
|
expected_pid = mapper.window_pid(window)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
retired_window_ids = set(existing_window_ids)
|
|
|
|
|
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
|
|
|
|
while process.poll() is None:
|
|
|
|
|
time.sleep(WINDOW_SCALING_EVENT_TIMEOUT_SECONDS)
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if now < next_focus_repair_at:
|
|
|
|
|
continue
|
|
|
|
|
if diagnostics is None:
|
|
|
|
|
_repair_game_window_focus(mapper, window)
|
|
|
|
|
else:
|
|
|
|
|
resolved_window = _resolve_direct_game_window(
|
|
|
|
|
mapper,
|
|
|
|
|
window,
|
|
|
|
|
window_title=window_title,
|
|
|
|
|
excluded_window_ids=retired_window_ids,
|
|
|
|
|
expected_pid=expected_pid,
|
|
|
|
|
diagnostics=diagnostics,
|
|
|
|
|
)
|
|
|
|
|
if resolved_window is not None:
|
|
|
|
|
if resolved_window != window:
|
|
|
|
|
retired_window_ids.add(window)
|
|
|
|
|
window = resolved_window
|
|
|
|
|
_repair_game_window_focus(
|
|
|
|
|
mapper,
|
|
|
|
|
window,
|
|
|
|
|
diagnostics=diagnostics,
|
|
|
|
|
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
@@ -2120,7 +2325,8 @@ def _guard_game_window_startup(
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""Recover Wine's login-screen window transition without hiding the client."""
|
|
|
|
|
|
|
|
|
|
deadline = time.monotonic() + WINDOW_STARTUP_GUARD_SECONDS
|
|
|
|
|
started_at = time.monotonic()
|
|
|
|
|
deadline = started_at + WINDOW_STARTUP_GUARD_SECONDS
|
|
|
|
|
try:
|
|
|
|
|
initial_borderless = (
|
|
|
|
|
mapper.window_is_borderless(window) if borderless else None
|
|
|
|
|
@@ -2150,8 +2356,10 @@ def _guard_game_window_startup(
|
|
|
|
|
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
|
|
|
|
)
|
|
|
|
|
recovery_count = 0
|
|
|
|
|
next_focus_repair_at = started_at + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
|
|
|
|
while process.poll() is None:
|
|
|
|
|
if time.monotonic() >= deadline:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if now >= deadline:
|
|
|
|
|
return True
|
|
|
|
|
try:
|
|
|
|
|
state = mapper.window_state(window)
|
|
|
|
|
@@ -2193,6 +2401,17 @@ def _guard_game_window_startup(
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
if state is None or state == _X_WM_STATE_WITHDRAWN:
|
|
|
|
|
# Let the focus monitor look for the replacement without waiting
|
|
|
|
|
# out the startup guard on a window which no longer exists.
|
|
|
|
|
return False
|
|
|
|
|
if now >= next_focus_repair_at:
|
|
|
|
|
_repair_game_window_focus(
|
|
|
|
|
mapper,
|
|
|
|
|
window,
|
|
|
|
|
**({"diagnostics": diagnostics} if diagnostics is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
next_focus_repair_at = now + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
|
|
|
|
if (
|
|
|
|
|
not entered_screen_filling_mode
|
|
|
|
|
and not login_style_reset
|
|
|
|
|
@@ -2200,13 +2419,18 @@ def _guard_game_window_startup(
|
|
|
|
|
time.sleep(WINDOW_STARTUP_POLL_SECONDS)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if state != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
|
|
|
|
return False
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
recovery_count += 1
|
|
|
|
|
if entered_screen_filling_mode:
|
|
|
|
|
reason = "entered fullscreen/maximized mode"
|
|
|
|
|
else:
|
|
|
|
|
reason = "changed its window layout"
|
|
|
|
|
print(
|
|
|
|
|
f"Game startup window {reason}; restoring and refocusing it.",
|
|
|
|
|
f"Game startup window {reason}; restoring its window layout.",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
@@ -2219,16 +2443,16 @@ def _guard_game_window_startup(
|
|
|
|
|
**_sample_window_diagnostic_details(diagnostics, mapper, window),
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
mapper.activate_window(window)
|
|
|
|
|
time.sleep(WINDOW_ACTIVATION_DELAY_SECONDS)
|
|
|
|
|
if mapper.window_state(window) != _X_WM_STATE_NORMAL or not mapper.window_is_active(window):
|
|
|
|
|
return False
|
|
|
|
|
_prepare_game_window(
|
|
|
|
|
mapper,
|
|
|
|
|
window,
|
|
|
|
|
borderless=borderless,
|
|
|
|
|
suppress_wm_ping=False,
|
|
|
|
|
raise_window=False,
|
|
|
|
|
)
|
|
|
|
|
mapper.activate_window(window)
|
|
|
|
|
mapper.focus_window(window)
|
|
|
|
|
_repair_game_window_focus(mapper, window)
|
|
|
|
|
if borderless:
|
|
|
|
|
borderless_was_applied = (
|
|
|
|
|
mapper.window_is_borderless(window) is True
|
|
|
|
|
@@ -2322,6 +2546,7 @@ def run_game_window(
|
|
|
|
|
finally:
|
|
|
|
|
diagnostics.close(process_started=True, managed=False)
|
|
|
|
|
game_window: int | None = None
|
|
|
|
|
game_window_pid: int | None = None
|
|
|
|
|
game_title: str | None = None
|
|
|
|
|
game_window_created_at: float | None = None
|
|
|
|
|
deadline = time.monotonic() + WINDOW_DISCOVERY_TIMEOUT_SECONDS
|
|
|
|
|
@@ -2336,6 +2561,10 @@ def run_game_window(
|
|
|
|
|
if found is not None:
|
|
|
|
|
window, title = found
|
|
|
|
|
game_window = window
|
|
|
|
|
try:
|
|
|
|
|
game_window_pid = mapper.window_pid(window)
|
|
|
|
|
except Exception:
|
|
|
|
|
game_window_pid = None
|
|
|
|
|
game_title = title
|
|
|
|
|
game_window_created_at = now
|
|
|
|
|
_apply_game_window_identity(mapper, window)
|
|
|
|
|
@@ -2381,7 +2610,12 @@ def run_game_window(
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
if _gamescope_host_focus_is_allowed():
|
|
|
|
|
_focus_game_window(mapper, game_window)
|
|
|
|
|
_focus_game_window(
|
|
|
|
|
mapper,
|
|
|
|
|
game_window,
|
|
|
|
|
activate=False,
|
|
|
|
|
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
"Game presentation is isolated; Gamescope handles "
|
|
|
|
|
"aspect-preserving outer-window resizing.",
|
|
|
|
|
@@ -2405,6 +2639,7 @@ def run_game_window(
|
|
|
|
|
game_window,
|
|
|
|
|
borderless=borderless,
|
|
|
|
|
suppress_wm_ping=suppress_wm_ping,
|
|
|
|
|
raise_window=False,
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
f"Game window ready: {game_title or window_title}",
|
|
|
|
|
@@ -2422,7 +2657,11 @@ def run_game_window(
|
|
|
|
|
game_window,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
_focus_game_window(mapper, game_window)
|
|
|
|
|
_focus_game_window(
|
|
|
|
|
mapper,
|
|
|
|
|
game_window,
|
|
|
|
|
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
startup_stable = _guard_game_window_startup(
|
|
|
|
|
mapper,
|
|
|
|
|
process,
|
|
|
|
|
@@ -2444,31 +2683,32 @@ def run_game_window(
|
|
|
|
|
mapper,
|
|
|
|
|
process,
|
|
|
|
|
game_window,
|
|
|
|
|
window_title=window_title,
|
|
|
|
|
existing_window_ids=existing_window_ids,
|
|
|
|
|
expected_pid=game_window_pid,
|
|
|
|
|
**(
|
|
|
|
|
{"diagnostics": diagnostic_sink}
|
|
|
|
|
if diagnostic_sink is not None
|
|
|
|
|
else {}
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
if not borderless:
|
|
|
|
|
if allow_window_resizing and not startup_stable:
|
|
|
|
|
print(
|
|
|
|
|
"Warning: live window resizing was not enabled because the "
|
|
|
|
|
"startup window did not stabilize.",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return _maintain_game_window_focus(
|
|
|
|
|
mapper,
|
|
|
|
|
process,
|
|
|
|
|
game_window,
|
|
|
|
|
**(
|
|
|
|
|
{"diagnostics": diagnostic_sink}
|
|
|
|
|
if diagnostic_sink is not None
|
|
|
|
|
else {}
|
|
|
|
|
),
|
|
|
|
|
if allow_window_resizing and not startup_stable and not borderless:
|
|
|
|
|
print(
|
|
|
|
|
"Warning: live window resizing was not enabled because the "
|
|
|
|
|
"startup window did not stabilize.",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
# Borderless clients need the same active-window-only input repair.
|
|
|
|
|
return _maintain_game_window_focus(
|
|
|
|
|
mapper,
|
|
|
|
|
process,
|
|
|
|
|
game_window,
|
|
|
|
|
window_title=window_title,
|
|
|
|
|
existing_window_ids=existing_window_ids,
|
|
|
|
|
expected_pid=game_window_pid,
|
|
|
|
|
**({"diagnostics": diagnostic_sink} if diagnostic_sink is not None else {}),
|
|
|
|
|
)
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
if process.poll() is None:
|
|
|
|
|
print(
|
|
|
|
|
@@ -2482,6 +2722,7 @@ def run_game_window(
|
|
|
|
|
game_window,
|
|
|
|
|
borderless=borderless,
|
|
|
|
|
suppress_wm_ping=suppress_wm_ping,
|
|
|
|
|
raise_window=False,
|
|
|
|
|
)
|
|
|
|
|
return int(process.wait())
|
|
|
|
|
finally:
|
|
|
|
|
|