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 ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Collection, Iterable, Sequence
|
||||
from collections.abc import Collection, Iterable, Mapping, Sequence
|
||||
|
||||
from .desktop_identity import (
|
||||
APPLICATION_NAME,
|
||||
@@ -44,12 +46,15 @@ _X_PROPERTY_CHANGE_MASK = 1 << 22
|
||||
_X_SUBSTRUCTURE_NOTIFY_MASK = 1 << 19
|
||||
_X_SUBSTRUCTURE_REDIRECT_MASK = 1 << 20
|
||||
_X_WM_STATE_WITHDRAWN = 0
|
||||
_X_WM_STATE_NORMAL = 1
|
||||
_X_WM_STATE_ICONIC = 3
|
||||
_NET_WM_STATE_REMOVE = 0
|
||||
_NET_WM_STATE_SOURCE_APPLICATION = 1
|
||||
_NET_WM_BYPASS_COMPOSITOR_PREFER_COMPOSITING = 2
|
||||
WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS = 1.0
|
||||
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_MAX_SIZE = 1 << 5
|
||||
_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)
|
||||
|
||||
|
||||
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:
|
||||
"""Keep a stale X11 window race from terminating the mapper process."""
|
||||
return 0
|
||||
@@ -555,21 +568,14 @@ class _X11:
|
||||
and updated_normal_hints != normal_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)
|
||||
allowed_actions_with_scaling = self._allowed_actions_with_scaling(
|
||||
allowed_actions,
|
||||
)
|
||||
allowed_actions_changed = (
|
||||
allowed_actions is not None
|
||||
and allowed_actions_with_scaling is not None
|
||||
and allowed_actions_with_scaling != allowed_actions
|
||||
)
|
||||
if (
|
||||
not normal_changed
|
||||
and not motif_changed
|
||||
and not allowed_actions_changed
|
||||
):
|
||||
return not confirm or self.window_allows_scaling(window)
|
||||
if not normal_changed and not motif_changed:
|
||||
if not confirm:
|
||||
return True
|
||||
# ``_NET_WM_ALLOWED_ACTIONS`` belongs to the window manager. Wait for
|
||||
# its answer even when the client hints were already resizable; Muffin
|
||||
# can publish the property shortly after the SDL window is discovered.
|
||||
self.library.XSync(self.display, 0)
|
||||
return self._wait_for_scaling_actions(window)
|
||||
|
||||
confirmed = False
|
||||
try:
|
||||
@@ -587,13 +593,6 @@ class _X11:
|
||||
b"_MOTIF_WM_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:
|
||||
self.library.XFlush(self.display)
|
||||
confirmed = True
|
||||
@@ -624,16 +623,6 @@ class _X11:
|
||||
)
|
||||
except Exception as 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:
|
||||
self.library.XSync(self.display, 0)
|
||||
except Exception as exc:
|
||||
@@ -665,36 +654,6 @@ class _X11:
|
||||
vertical,
|
||||
}.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:
|
||||
deadline = time.monotonic() + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS
|
||||
while not self.window_allows_scaling(window):
|
||||
@@ -1004,6 +963,14 @@ class _X11:
|
||||
active = self._long_property(root, b"_NET_ACTIVE_WINDOW", 1)
|
||||
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:
|
||||
"""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.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
||||
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 = [
|
||||
ctypes.c_void_p,
|
||||
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:
|
||||
"""Restore nested focus without asking the desktop to activate Gamescope."""
|
||||
|
||||
if not _gamescope_host_focus_is_allowed():
|
||||
return False
|
||||
try:
|
||||
if mapper.window_state(window) == _X_WM_STATE_ICONIC:
|
||||
return False
|
||||
@@ -1477,6 +1454,31 @@ def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||
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(
|
||||
mapper: _X11,
|
||||
window: int,
|
||||
@@ -1851,7 +1853,8 @@ def run_game_window(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
_focus_game_window(mapper, game_window)
|
||||
if _gamescope_host_focus_is_allowed():
|
||||
_focus_game_window(mapper, game_window)
|
||||
print(
|
||||
"Game presentation is isolated; Gamescope handles "
|
||||
"aspect-preserving outer-window resizing.",
|
||||
@@ -1944,13 +1947,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
if command and command[0] == "--":
|
||||
command = command[1:]
|
||||
try:
|
||||
return run_game_window(
|
||||
command,
|
||||
arguments.window_title,
|
||||
borderless=arguments.borderless,
|
||||
allow_window_resizing=arguments.allow_window_resizing,
|
||||
gamescope_scaling=arguments.gamescope_scaling,
|
||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||
return _process_exit_status(
|
||||
run_game_window(
|
||||
command,
|
||||
arguments.window_title,
|
||||
borderless=arguments.borderless,
|
||||
allow_window_resizing=arguments.allow_window_resizing,
|
||||
gamescope_scaling=arguments.gamescope_scaling,
|
||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||
)
|
||||
)
|
||||
except (GameboxRuntimeError, OSError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
|
||||
@@ -14,7 +14,13 @@ import tempfile
|
||||
import time
|
||||
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_POLL_SECONDS = 0.05
|
||||
@@ -22,6 +28,8 @@ RESIZE_STEPS_PER_GAME_CANVAS = 20
|
||||
MIN_RESIZE_INCREMENT = 8
|
||||
MAX_RESIZE_INCREMENT = 96
|
||||
CHILD_STATUS_POLL_SECONDS = 0.05
|
||||
HOST_FOCUS_HEARTBEAT_SECONDS = 0.2
|
||||
HOST_REICONIFY_INTERVAL_SECONDS = 0.2
|
||||
GAMESCOPE_VERSION_TIMEOUT_SECONDS = 1.0
|
||||
WAYLAND_BACKING_FIX_VERSION = (3, 16, 28)
|
||||
BUNDLED_GAMESCOPE_VERSION = (3, 16, 20)
|
||||
@@ -43,6 +51,101 @@ class GamescopeRuntimeError(RuntimeError):
|
||||
"""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]:
|
||||
"""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(
|
||||
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:
|
||||
"""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:
|
||||
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 process.poll() is None:
|
||||
process.kill()
|
||||
@@ -260,7 +377,9 @@ def _wait_for_optimized_gamescope(
|
||||
return _process_exit_status(child_status)
|
||||
gamescope_status = process.poll()
|
||||
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(
|
||||
gamescope_status if child_status is None else child_status
|
||||
)
|
||||
@@ -283,6 +402,8 @@ def _tune_process_window(
|
||||
process: subprocess.Popen[bytes],
|
||||
width: int,
|
||||
height: int,
|
||||
*,
|
||||
host_focus_publisher: _HostFocusPublisher | None = None,
|
||||
) -> tuple[int, int] | None:
|
||||
"""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:
|
||||
window = mapper.find_window_by_pid(process.pid)
|
||||
if window is not None:
|
||||
if host_focus_publisher is not None:
|
||||
host_focus_publisher.update(mapper, window)
|
||||
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(
|
||||
window,
|
||||
(width, height),
|
||||
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)
|
||||
if scaling_enabled and increments_applied and compositing_preferred:
|
||||
return increments
|
||||
@@ -325,6 +450,7 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
||||
) = _select_wayland_gamescope(selected_command[0])
|
||||
mapper = _open_host_x11() if backend == "sdl" else None
|
||||
status_file: Path | None = None
|
||||
host_focus_publisher: _HostFocusPublisher | None = None
|
||||
try:
|
||||
candidate_status_file = _create_child_status_file()
|
||||
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
|
||||
# screen-filling sizes and visibly flicker while Gamescope is scaling.
|
||||
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:
|
||||
# The game asks its X11 WM to iconify the first client while entering
|
||||
# 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:
|
||||
if mapper is not None:
|
||||
mapper.close()
|
||||
if host_focus_publisher is not None:
|
||||
host_focus_publisher.close()
|
||||
if status_file is not None:
|
||||
status_file.unlink(missing_ok=True)
|
||||
raise GamescopeRuntimeError(f"could not start Gamescope: {exc}") from exc
|
||||
@@ -396,23 +541,21 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
||||
)
|
||||
elif mapper is not None:
|
||||
try:
|
||||
try:
|
||||
increments = _tune_process_window(
|
||||
mapper,
|
||||
process,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
except Exception as exc:
|
||||
increments = None
|
||||
print(
|
||||
f"Warning: Gamescope resize tuning failed: {exc}; "
|
||||
"continuing normally.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
finally:
|
||||
mapper.close()
|
||||
increments = _tune_process_window(
|
||||
mapper,
|
||||
process,
|
||||
width,
|
||||
height,
|
||||
host_focus_publisher=host_focus_publisher,
|
||||
)
|
||||
except Exception as exc:
|
||||
increments = None
|
||||
print(
|
||||
f"Warning: Gamescope resize tuning failed: {exc}; "
|
||||
"continuing normally.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
if increments is not None:
|
||||
print(
|
||||
"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,
|
||||
)
|
||||
try:
|
||||
if status_file is not None:
|
||||
return _wait_for_optimized_gamescope(process, status_file)
|
||||
return _process_exit_status(int(process.wait()))
|
||||
return _wait_for_optimized_gamescope(
|
||||
process,
|
||||
status_file,
|
||||
host_focus_publisher=host_focus_publisher,
|
||||
host_mapper=mapper,
|
||||
)
|
||||
except BaseException:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
@@ -449,6 +595,10 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
|
||||
process.wait()
|
||||
raise
|
||||
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:
|
||||
status_file.unlink(missing_ok=True)
|
||||
|
||||
|
||||
@@ -509,6 +509,9 @@ def run_game(
|
||||
output_line_filter=output_line_filter,
|
||||
pass_fds=pass_fds,
|
||||
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