Fixed more issues with Cinnamon window resizing and popups.
This commit is contained in:
@@ -5,11 +5,13 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import ctypes
|
import ctypes
|
||||||
import ctypes.util
|
import ctypes.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections.abc import Collection, Iterable, Sequence
|
from collections.abc import Collection, Iterable, Mapping, Sequence
|
||||||
|
|
||||||
from .desktop_identity import (
|
from .desktop_identity import (
|
||||||
APPLICATION_NAME,
|
APPLICATION_NAME,
|
||||||
@@ -44,12 +46,15 @@ _X_PROPERTY_CHANGE_MASK = 1 << 22
|
|||||||
_X_SUBSTRUCTURE_NOTIFY_MASK = 1 << 19
|
_X_SUBSTRUCTURE_NOTIFY_MASK = 1 << 19
|
||||||
_X_SUBSTRUCTURE_REDIRECT_MASK = 1 << 20
|
_X_SUBSTRUCTURE_REDIRECT_MASK = 1 << 20
|
||||||
_X_WM_STATE_WITHDRAWN = 0
|
_X_WM_STATE_WITHDRAWN = 0
|
||||||
|
_X_WM_STATE_NORMAL = 1
|
||||||
_X_WM_STATE_ICONIC = 3
|
_X_WM_STATE_ICONIC = 3
|
||||||
_NET_WM_STATE_REMOVE = 0
|
_NET_WM_STATE_REMOVE = 0
|
||||||
_NET_WM_STATE_SOURCE_APPLICATION = 1
|
_NET_WM_STATE_SOURCE_APPLICATION = 1
|
||||||
_NET_WM_BYPASS_COMPOSITOR_PREFER_COMPOSITING = 2
|
_NET_WM_BYPASS_COMPOSITOR_PREFER_COMPOSITING = 2
|
||||||
WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS = 1.0
|
WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS = 1.0
|
||||||
GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS = 0.25
|
GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS = 0.25
|
||||||
|
GAMESCOPE_HOST_FOCUS_STATE_ENV = "PTLINUX_GAMESCOPE_HOST_FOCUS_STATE"
|
||||||
|
GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS = 0.6
|
||||||
_P_MIN_SIZE = 1 << 4
|
_P_MIN_SIZE = 1 << 4
|
||||||
_P_MAX_SIZE = 1 << 5
|
_P_MAX_SIZE = 1 << 5
|
||||||
_P_RESIZE_INC = 1 << 6
|
_P_RESIZE_INC = 1 << 6
|
||||||
@@ -64,6 +69,14 @@ _MOTIF_DECOR_MAXIMIZE = 1 << 6
|
|||||||
_X_ERROR_HANDLER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
_X_ERROR_HANDLER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_exit_status(returncode: int) -> int:
|
||||||
|
"""Convert Popen's negative signal code before passing it through SystemExit."""
|
||||||
|
|
||||||
|
if returncode < 0:
|
||||||
|
return 128 + min(-returncode, 127)
|
||||||
|
return min(returncode, 255)
|
||||||
|
|
||||||
|
|
||||||
def _ignore_x11_error(_display: object, _event: object) -> int:
|
def _ignore_x11_error(_display: object, _event: object) -> int:
|
||||||
"""Keep a stale X11 window race from terminating the mapper process."""
|
"""Keep a stale X11 window race from terminating the mapper process."""
|
||||||
return 0
|
return 0
|
||||||
@@ -555,21 +568,14 @@ class _X11:
|
|||||||
and updated_normal_hints != normal_hints
|
and updated_normal_hints != normal_hints
|
||||||
)
|
)
|
||||||
motif_changed = motif_hints is not None and updated_motif_hints != motif_hints
|
motif_changed = motif_hints is not None and updated_motif_hints != motif_hints
|
||||||
allowed_actions = self._long_property(window, b"_NET_WM_ALLOWED_ACTIONS", 64)
|
if not normal_changed and not motif_changed:
|
||||||
allowed_actions_with_scaling = self._allowed_actions_with_scaling(
|
if not confirm:
|
||||||
allowed_actions,
|
return True
|
||||||
)
|
# ``_NET_WM_ALLOWED_ACTIONS`` belongs to the window manager. Wait for
|
||||||
allowed_actions_changed = (
|
# its answer even when the client hints were already resizable; Muffin
|
||||||
allowed_actions is not None
|
# can publish the property shortly after the SDL window is discovered.
|
||||||
and allowed_actions_with_scaling is not None
|
self.library.XSync(self.display, 0)
|
||||||
and allowed_actions_with_scaling != allowed_actions
|
return self._wait_for_scaling_actions(window)
|
||||||
)
|
|
||||||
if (
|
|
||||||
not normal_changed
|
|
||||||
and not motif_changed
|
|
||||||
and not allowed_actions_changed
|
|
||||||
):
|
|
||||||
return not confirm or self.window_allows_scaling(window)
|
|
||||||
|
|
||||||
confirmed = False
|
confirmed = False
|
||||||
try:
|
try:
|
||||||
@@ -587,13 +593,6 @@ class _X11:
|
|||||||
b"_MOTIF_WM_HINTS",
|
b"_MOTIF_WM_HINTS",
|
||||||
updated_motif_hints,
|
updated_motif_hints,
|
||||||
)
|
)
|
||||||
if allowed_actions_changed and allowed_actions_with_scaling is not None:
|
|
||||||
self._replace_long_property(
|
|
||||||
window,
|
|
||||||
b"_NET_WM_ALLOWED_ACTIONS",
|
|
||||||
b"_ATOM",
|
|
||||||
allowed_actions_with_scaling,
|
|
||||||
)
|
|
||||||
if not confirm:
|
if not confirm:
|
||||||
self.library.XFlush(self.display)
|
self.library.XFlush(self.display)
|
||||||
confirmed = True
|
confirmed = True
|
||||||
@@ -624,16 +623,6 @@ class _X11:
|
|||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
restore_errors.append(exc)
|
restore_errors.append(exc)
|
||||||
if allowed_actions_changed and allowed_actions is not None:
|
|
||||||
try:
|
|
||||||
self._replace_long_property(
|
|
||||||
window,
|
|
||||||
b"_NET_WM_ALLOWED_ACTIONS",
|
|
||||||
b"_ATOM",
|
|
||||||
allowed_actions,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
restore_errors.append(exc)
|
|
||||||
try:
|
try:
|
||||||
self.library.XSync(self.display, 0)
|
self.library.XSync(self.display, 0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -665,36 +654,6 @@ class _X11:
|
|||||||
vertical,
|
vertical,
|
||||||
}.issubset(actions)
|
}.issubset(actions)
|
||||||
|
|
||||||
def _allowed_actions_with_scaling(
|
|
||||||
self,
|
|
||||||
actions: tuple[int, ...] | None,
|
|
||||||
) -> tuple[int, ...] | None:
|
|
||||||
if actions is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
resize = self._intern_atom(
|
|
||||||
b"_NET_WM_ACTION_RESIZE",
|
|
||||||
only_if_exists=True,
|
|
||||||
)
|
|
||||||
horizontal = self._intern_atom(
|
|
||||||
b"_NET_WM_ACTION_MAXIMIZE_HORZ",
|
|
||||||
only_if_exists=True,
|
|
||||||
)
|
|
||||||
vertical = self._intern_atom(
|
|
||||||
b"_NET_WM_ACTION_MAXIMIZE_VERT",
|
|
||||||
only_if_exists=True,
|
|
||||||
)
|
|
||||||
if resize == 0 or horizontal == 0 or vertical == 0:
|
|
||||||
return actions
|
|
||||||
|
|
||||||
updated_actions = list(actions)
|
|
||||||
known_actions = set(actions)
|
|
||||||
for atom in (resize, horizontal, vertical):
|
|
||||||
if atom not in known_actions:
|
|
||||||
updated_actions.append(atom)
|
|
||||||
known_actions.add(atom)
|
|
||||||
return tuple(updated_actions)
|
|
||||||
|
|
||||||
def _wait_for_scaling_actions(self, window: int) -> bool:
|
def _wait_for_scaling_actions(self, window: int) -> bool:
|
||||||
deadline = time.monotonic() + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS
|
deadline = time.monotonic() + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS
|
||||||
while not self.window_allows_scaling(window):
|
while not self.window_allows_scaling(window):
|
||||||
@@ -1004,6 +963,14 @@ class _X11:
|
|||||||
active = self._long_property(root, b"_NET_ACTIVE_WINDOW", 1)
|
active = self._long_property(root, b"_NET_ACTIVE_WINDOW", 1)
|
||||||
return bool(active and active[0] == window)
|
return bool(active and active[0] == window)
|
||||||
|
|
||||||
|
def minimize_window(self, window: int) -> bool:
|
||||||
|
"""Ask the host window manager to keep a remapped client minimized."""
|
||||||
|
|
||||||
|
screen = self.library.XDefaultScreen(self.display)
|
||||||
|
accepted = self.library.XIconifyWindow(self.display, window, screen) != 0
|
||||||
|
self.library.XSync(self.display, 0)
|
||||||
|
return accepted
|
||||||
|
|
||||||
def activate_window(self, window: int) -> bool:
|
def activate_window(self, window: int) -> bool:
|
||||||
"""Ask the window manager to restore and activate one client."""
|
"""Ask the window manager to restore and activate one client."""
|
||||||
|
|
||||||
@@ -1174,6 +1141,14 @@ class _X11:
|
|||||||
self.library.XOpenDisplay.restype = ctypes.c_void_p
|
self.library.XOpenDisplay.restype = ctypes.c_void_p
|
||||||
self.library.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
self.library.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
||||||
self.library.XDefaultRootWindow.restype = ctypes.c_ulong
|
self.library.XDefaultRootWindow.restype = ctypes.c_ulong
|
||||||
|
self.library.XDefaultScreen.argtypes = [ctypes.c_void_p]
|
||||||
|
self.library.XDefaultScreen.restype = ctypes.c_int
|
||||||
|
self.library.XIconifyWindow.argtypes = [
|
||||||
|
ctypes.c_void_p,
|
||||||
|
ctypes.c_ulong,
|
||||||
|
ctypes.c_int,
|
||||||
|
]
|
||||||
|
self.library.XIconifyWindow.restype = ctypes.c_int
|
||||||
self.library.XQueryTree.argtypes = [
|
self.library.XQueryTree.argtypes = [
|
||||||
ctypes.c_void_p,
|
ctypes.c_void_p,
|
||||||
ctypes.c_ulong,
|
ctypes.c_ulong,
|
||||||
@@ -1457,6 +1432,8 @@ def _repair_game_window_focus(mapper: _X11, window: int) -> bool:
|
|||||||
def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||||
"""Restore nested focus without asking the desktop to activate Gamescope."""
|
"""Restore nested focus without asking the desktop to activate Gamescope."""
|
||||||
|
|
||||||
|
if not _gamescope_host_focus_is_allowed():
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
if mapper.window_state(window) == _X_WM_STATE_ICONIC:
|
if mapper.window_state(window) == _X_WM_STATE_ICONIC:
|
||||||
return False
|
return False
|
||||||
@@ -1477,6 +1454,31 @@ def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _gamescope_host_focus_is_allowed(
|
||||||
|
*,
|
||||||
|
environment: Mapping[str, str] | None = None,
|
||||||
|
now: float | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Honor the outer X11 window's current focus without trusting stale state."""
|
||||||
|
|
||||||
|
env = os.environ if environment is None else environment
|
||||||
|
state_path = env.get(GAMESCOPE_HOST_FOCUS_STATE_ENV, "").strip()
|
||||||
|
if not state_path:
|
||||||
|
# Direct invocations and native Wayland Gamescope have no X11 host-state
|
||||||
|
# bridge. Retain their existing nested focus behavior.
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
fields = Path(state_path).read_text(encoding="ascii").split()
|
||||||
|
if len(fields) != 2 or fields[0] != "1":
|
||||||
|
return False
|
||||||
|
published_at = float(fields[1])
|
||||||
|
except (OSError, UnicodeError, ValueError):
|
||||||
|
return False
|
||||||
|
observed_at = time.monotonic() if now is None else now
|
||||||
|
age = observed_at - published_at
|
||||||
|
return 0.0 <= age <= GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS
|
||||||
|
|
||||||
|
|
||||||
def _resolve_gamescope_game_window(
|
def _resolve_gamescope_game_window(
|
||||||
mapper: _X11,
|
mapper: _X11,
|
||||||
window: int,
|
window: int,
|
||||||
@@ -1851,6 +1853,7 @@ def run_game_window(
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
if _gamescope_host_focus_is_allowed():
|
||||||
_focus_game_window(mapper, game_window)
|
_focus_game_window(mapper, game_window)
|
||||||
print(
|
print(
|
||||||
"Game presentation is isolated; Gamescope handles "
|
"Game presentation is isolated; Gamescope handles "
|
||||||
@@ -1944,7 +1947,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
if command and command[0] == "--":
|
if command and command[0] == "--":
|
||||||
command = command[1:]
|
command = command[1:]
|
||||||
try:
|
try:
|
||||||
return run_game_window(
|
return _process_exit_status(
|
||||||
|
run_game_window(
|
||||||
command,
|
command,
|
||||||
arguments.window_title,
|
arguments.window_title,
|
||||||
borderless=arguments.borderless,
|
borderless=arguments.borderless,
|
||||||
@@ -1952,6 +1956,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
gamescope_scaling=arguments.gamescope_scaling,
|
gamescope_scaling=arguments.gamescope_scaling,
|
||||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
except (GameboxRuntimeError, OSError) as exc:
|
except (GameboxRuntimeError, OSError) as exc:
|
||||||
print(f"Error: {exc}", file=sys.stderr)
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
from .gamebox_runtime import GameboxRuntimeError, _X11
|
from .gamebox_runtime import (
|
||||||
|
GAMESCOPE_HOST_FOCUS_STATE_ENV,
|
||||||
|
GameboxRuntimeError,
|
||||||
|
_X11,
|
||||||
|
_X_WM_STATE_ICONIC,
|
||||||
|
_X_WM_STATE_NORMAL,
|
||||||
|
)
|
||||||
|
|
||||||
WINDOW_DISCOVERY_TIMEOUT_SECONDS = 10.0
|
WINDOW_DISCOVERY_TIMEOUT_SECONDS = 10.0
|
||||||
WINDOW_DISCOVERY_POLL_SECONDS = 0.05
|
WINDOW_DISCOVERY_POLL_SECONDS = 0.05
|
||||||
@@ -22,6 +28,8 @@ RESIZE_STEPS_PER_GAME_CANVAS = 20
|
|||||||
MIN_RESIZE_INCREMENT = 8
|
MIN_RESIZE_INCREMENT = 8
|
||||||
MAX_RESIZE_INCREMENT = 96
|
MAX_RESIZE_INCREMENT = 96
|
||||||
CHILD_STATUS_POLL_SECONDS = 0.05
|
CHILD_STATUS_POLL_SECONDS = 0.05
|
||||||
|
HOST_FOCUS_HEARTBEAT_SECONDS = 0.2
|
||||||
|
HOST_REICONIFY_INTERVAL_SECONDS = 0.2
|
||||||
GAMESCOPE_VERSION_TIMEOUT_SECONDS = 1.0
|
GAMESCOPE_VERSION_TIMEOUT_SECONDS = 1.0
|
||||||
WAYLAND_BACKING_FIX_VERSION = (3, 16, 28)
|
WAYLAND_BACKING_FIX_VERSION = (3, 16, 28)
|
||||||
BUNDLED_GAMESCOPE_VERSION = (3, 16, 20)
|
BUNDLED_GAMESCOPE_VERSION = (3, 16, 20)
|
||||||
@@ -43,6 +51,101 @@ class GamescopeRuntimeError(RuntimeError):
|
|||||||
"""Raised when the Gamescope process cannot be started."""
|
"""Raised when the Gamescope process cannot be started."""
|
||||||
|
|
||||||
|
|
||||||
|
class _HostFocusPublisher:
|
||||||
|
"""Publish the host WM's focus decision to the nested game supervisor."""
|
||||||
|
|
||||||
|
def __init__(self, file_descriptor: int, path: Path) -> None:
|
||||||
|
self.file_descriptor = file_descriptor
|
||||||
|
self.path = path
|
||||||
|
self._last_active: bool | None = None
|
||||||
|
self._last_published_at = 0.0
|
||||||
|
self._last_reiconify_at = 0.0
|
||||||
|
self._seen_normal_state = False
|
||||||
|
self._minimized_latched = False
|
||||||
|
self._publish(False, force=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls) -> _HostFocusPublisher:
|
||||||
|
runtime_directory = os.environ.get("XDG_RUNTIME_DIR")
|
||||||
|
try:
|
||||||
|
file_descriptor, path = tempfile.mkstemp(
|
||||||
|
prefix="ptlinux-gamescope-focus-",
|
||||||
|
dir=runtime_directory,
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
file_descriptor, path = tempfile.mkstemp(
|
||||||
|
prefix="ptlinux-gamescope-focus-",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return cls(file_descriptor, Path(path))
|
||||||
|
except BaseException:
|
||||||
|
os.close(file_descriptor)
|
||||||
|
Path(path).unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def update(self, mapper: _X11, window: int) -> None:
|
||||||
|
now = time.monotonic()
|
||||||
|
try:
|
||||||
|
state = mapper.window_state(window)
|
||||||
|
active = state == _X_WM_STATE_NORMAL and mapper.window_is_active(window)
|
||||||
|
except Exception:
|
||||||
|
state = None
|
||||||
|
active = False
|
||||||
|
if state == _X_WM_STATE_ICONIC and self._seen_normal_state:
|
||||||
|
self._minimized_latched = True
|
||||||
|
elif state == _X_WM_STATE_NORMAL:
|
||||||
|
self._seen_normal_state = True
|
||||||
|
if self._minimized_latched:
|
||||||
|
if active:
|
||||||
|
# A normal, active window was explicitly restored by the
|
||||||
|
# desktop. Resume nested focus repair for it.
|
||||||
|
self._minimized_latched = False
|
||||||
|
elif now - self._last_reiconify_at >= HOST_REICONIFY_INTERVAL_SECONDS:
|
||||||
|
# Gamescope's SDL visibility handoff uses XMapRaised. If it
|
||||||
|
# remaps an inactive window after a user minimize, preserve
|
||||||
|
# the user's state instead of letting it cover other apps.
|
||||||
|
try:
|
||||||
|
mapper.minimize_window(window)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._last_reiconify_at = now
|
||||||
|
active = False
|
||||||
|
self._publish(active, now=now)
|
||||||
|
|
||||||
|
def _publish(
|
||||||
|
self,
|
||||||
|
active: bool,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
now: float | None = None,
|
||||||
|
) -> None:
|
||||||
|
published_at = time.monotonic() if now is None else now
|
||||||
|
if (
|
||||||
|
not force
|
||||||
|
and active == self._last_active
|
||||||
|
and published_at - self._last_published_at < HOST_FOCUS_HEARTBEAT_SECONDS
|
||||||
|
):
|
||||||
|
return
|
||||||
|
payload = f"{int(active)} {published_at:.9f}\n".encode("ascii")
|
||||||
|
try:
|
||||||
|
os.lseek(self.file_descriptor, 0, os.SEEK_SET)
|
||||||
|
# Truncating first makes an interrupted update fail closed in the
|
||||||
|
# nested reader instead of leaving a stale active value behind.
|
||||||
|
os.ftruncate(self.file_descriptor, 0)
|
||||||
|
os.write(self.file_descriptor, payload)
|
||||||
|
except OSError:
|
||||||
|
self._last_active = None
|
||||||
|
return
|
||||||
|
self._last_active = active
|
||||||
|
self._last_published_at = published_at
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
try:
|
||||||
|
os.close(self.file_descriptor)
|
||||||
|
finally:
|
||||||
|
self.path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def resize_increments(width: int, height: int) -> tuple[int, int]:
|
def resize_increments(width: int, height: int) -> tuple[int, int]:
|
||||||
"""Return coarse-enough size steps to avoid a swapchain rebuild storm."""
|
"""Return coarse-enough size steps to avoid a swapchain rebuild storm."""
|
||||||
|
|
||||||
@@ -247,12 +350,26 @@ def _process_exit_status(returncode: int) -> int:
|
|||||||
|
|
||||||
def _wait_for_optimized_gamescope(
|
def _wait_for_optimized_gamescope(
|
||||||
process: subprocess.Popen[bytes],
|
process: subprocess.Popen[bytes],
|
||||||
status_file: Path,
|
status_file: Path | None,
|
||||||
|
*,
|
||||||
|
host_focus_publisher: _HostFocusPublisher | None = None,
|
||||||
|
host_mapper: _X11 | None = None,
|
||||||
|
host_window: int | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Return the real child status and bypass old Gamescope shutdown bugs."""
|
"""Monitor host focus and preserve the nested child's real exit status."""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
child_status = _read_child_status(status_file)
|
if host_focus_publisher is not None and host_mapper is not None:
|
||||||
|
if host_window is None:
|
||||||
|
try:
|
||||||
|
host_window = host_mapper.find_window_by_pid(process.pid)
|
||||||
|
except Exception:
|
||||||
|
host_window = None
|
||||||
|
if host_window is not None:
|
||||||
|
host_focus_publisher.update(host_mapper, host_window)
|
||||||
|
child_status = (
|
||||||
|
_read_child_status(status_file) if status_file is not None else None
|
||||||
|
)
|
||||||
if child_status is not None:
|
if child_status is not None:
|
||||||
if process.poll() is None:
|
if process.poll() is None:
|
||||||
process.kill()
|
process.kill()
|
||||||
@@ -260,7 +377,9 @@ def _wait_for_optimized_gamescope(
|
|||||||
return _process_exit_status(child_status)
|
return _process_exit_status(child_status)
|
||||||
gamescope_status = process.poll()
|
gamescope_status = process.poll()
|
||||||
if gamescope_status is not None:
|
if gamescope_status is not None:
|
||||||
child_status = _read_child_status(status_file)
|
child_status = (
|
||||||
|
_read_child_status(status_file) if status_file is not None else None
|
||||||
|
)
|
||||||
return _process_exit_status(
|
return _process_exit_status(
|
||||||
gamescope_status if child_status is None else child_status
|
gamescope_status if child_status is None else child_status
|
||||||
)
|
)
|
||||||
@@ -283,6 +402,8 @@ def _tune_process_window(
|
|||||||
process: subprocess.Popen[bytes],
|
process: subprocess.Popen[bytes],
|
||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
|
*,
|
||||||
|
host_focus_publisher: _HostFocusPublisher | None = None,
|
||||||
) -> tuple[int, int] | None:
|
) -> tuple[int, int] | None:
|
||||||
"""Stabilize resizing once the SDL host window becomes available."""
|
"""Stabilize resizing once the SDL host window becomes available."""
|
||||||
|
|
||||||
@@ -290,15 +411,19 @@ def _tune_process_window(
|
|||||||
while process.poll() is None and time.monotonic() < deadline:
|
while process.poll() is None and time.monotonic() < deadline:
|
||||||
window = mapper.find_window_by_pid(process.pid)
|
window = mapper.find_window_by_pid(process.pid)
|
||||||
if window is not None:
|
if window is not None:
|
||||||
|
if host_focus_publisher is not None:
|
||||||
|
host_focus_publisher.update(mapper, window)
|
||||||
increments = resize_increments(width, height)
|
increments = resize_increments(width, height)
|
||||||
# Some X11 window managers keep the maximize decoration hidden until
|
|
||||||
# both the size constraints and allowed actions advertise resizing.
|
|
||||||
scaling_enabled = mapper.enable_window_scaling(window)
|
|
||||||
increments_applied = mapper.set_resize_increments(
|
increments_applied = mapper.set_resize_increments(
|
||||||
window,
|
window,
|
||||||
(width, height),
|
(width, height),
|
||||||
increments,
|
increments,
|
||||||
)
|
)
|
||||||
|
# Apply every client-owned hint first, then wait for the window
|
||||||
|
# manager to publish resize and maximize actions from those hints.
|
||||||
|
scaling_enabled = mapper.enable_window_scaling(window)
|
||||||
|
if host_focus_publisher is not None:
|
||||||
|
host_focus_publisher.update(mapper, window)
|
||||||
compositing_preferred = mapper.prefer_composited_presentation(window)
|
compositing_preferred = mapper.prefer_composited_presentation(window)
|
||||||
if scaling_enabled and increments_applied and compositing_preferred:
|
if scaling_enabled and increments_applied and compositing_preferred:
|
||||||
return increments
|
return increments
|
||||||
@@ -325,6 +450,7 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
) = _select_wayland_gamescope(selected_command[0])
|
) = _select_wayland_gamescope(selected_command[0])
|
||||||
mapper = _open_host_x11() if backend == "sdl" else None
|
mapper = _open_host_x11() if backend == "sdl" else None
|
||||||
status_file: Path | None = None
|
status_file: Path | None = None
|
||||||
|
host_focus_publisher: _HostFocusPublisher | None = None
|
||||||
try:
|
try:
|
||||||
candidate_status_file = _create_child_status_file()
|
candidate_status_file = _create_child_status_file()
|
||||||
try:
|
try:
|
||||||
@@ -361,6 +487,23 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
# SDL otherwise requests compositor bypass, which can switch near
|
# SDL otherwise requests compositor bypass, which can switch near
|
||||||
# screen-filling sizes and visibly flicker while Gamescope is scaling.
|
# screen-filling sizes and visibly flicker while Gamescope is scaling.
|
||||||
environment["SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR"] = "0"
|
environment["SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR"] = "0"
|
||||||
|
# If host X11 monitoring cannot start, make the nested focus repair
|
||||||
|
# fail closed. An absent bridge retains legacy behavior for direct
|
||||||
|
# gamebox invocations and native Wayland Gamescope only.
|
||||||
|
environment[GAMESCOPE_HOST_FOCUS_STATE_ENV] = "unavailable"
|
||||||
|
if mapper is not None:
|
||||||
|
try:
|
||||||
|
publisher = _HostFocusPublisher.create()
|
||||||
|
except OSError as exc:
|
||||||
|
print(
|
||||||
|
f"Warning: Gamescope host-focus monitoring is unavailable: "
|
||||||
|
f"{exc}; nested focus repair will remain disabled.",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
host_focus_publisher = publisher
|
||||||
|
environment[GAMESCOPE_HOST_FOCUS_STATE_ENV] = str(publisher.path)
|
||||||
if using_bundled_gamescope:
|
if using_bundled_gamescope:
|
||||||
# The game asks its X11 WM to iconify the first client while entering
|
# The game asks its X11 WM to iconify the first client while entering
|
||||||
# the login screen. Our scoped Gamescope patch keeps that nested surface
|
# the login screen. Our scoped Gamescope patch keeps that nested surface
|
||||||
@@ -371,6 +514,8 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
if mapper is not None:
|
if mapper is not None:
|
||||||
mapper.close()
|
mapper.close()
|
||||||
|
if host_focus_publisher is not None:
|
||||||
|
host_focus_publisher.close()
|
||||||
if status_file is not None:
|
if status_file is not None:
|
||||||
status_file.unlink(missing_ok=True)
|
status_file.unlink(missing_ok=True)
|
||||||
raise GamescopeRuntimeError(f"could not start Gamescope: {exc}") from exc
|
raise GamescopeRuntimeError(f"could not start Gamescope: {exc}") from exc
|
||||||
@@ -395,13 +540,13 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
elif mapper is not None:
|
elif mapper is not None:
|
||||||
try:
|
|
||||||
try:
|
try:
|
||||||
increments = _tune_process_window(
|
increments = _tune_process_window(
|
||||||
mapper,
|
mapper,
|
||||||
process,
|
process,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
host_focus_publisher=host_focus_publisher,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
increments = None
|
increments = None
|
||||||
@@ -411,8 +556,6 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
mapper.close()
|
|
||||||
if increments is not None:
|
if increments is not None:
|
||||||
print(
|
print(
|
||||||
"Gamescope resize optimization active: the host window remains "
|
"Gamescope resize optimization active: the host window remains "
|
||||||
@@ -436,9 +579,12 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if status_file is not None:
|
return _wait_for_optimized_gamescope(
|
||||||
return _wait_for_optimized_gamescope(process, status_file)
|
process,
|
||||||
return _process_exit_status(int(process.wait()))
|
status_file,
|
||||||
|
host_focus_publisher=host_focus_publisher,
|
||||||
|
host_mapper=mapper,
|
||||||
|
)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
if process.poll() is None:
|
if process.poll() is None:
|
||||||
process.terminate()
|
process.terminate()
|
||||||
@@ -449,6 +595,10 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
|||||||
process.wait()
|
process.wait()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
|
if mapper is not None:
|
||||||
|
mapper.close()
|
||||||
|
if host_focus_publisher is not None:
|
||||||
|
host_focus_publisher.close()
|
||||||
if status_file is not None:
|
if status_file is not None:
|
||||||
status_file.unlink(missing_ok=True)
|
status_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -509,6 +509,9 @@ def run_game(
|
|||||||
output_line_filter=output_line_filter,
|
output_line_filter=output_line_filter,
|
||||||
pass_fds=pass_fds,
|
pass_fds=pass_fds,
|
||||||
telemetry_stage="game_start",
|
telemetry_stage="game_start",
|
||||||
|
# Play distinguishes a failed launch from a nonzero status returned after
|
||||||
|
# a visible game session. Its game-specific event retains that context.
|
||||||
|
report_failure_telemetry=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user