Fixed more issues with Cinnamon window resizing and popups.

This commit is contained in:
2026-09-08 22:46:53 +02:00
parent 1a120bbf86
commit 76bef94757
3 changed files with 257 additions and 99 deletions

View File

@@ -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,7 +1853,8 @@ def run_game_window(
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )
_focus_game_window(mapper, game_window) if _gamescope_host_focus_is_allowed():
_focus_game_window(mapper, game_window)
print( print(
"Game presentation is isolated; Gamescope handles " "Game presentation is isolated; Gamescope handles "
"aspect-preserving outer-window resizing.", "aspect-preserving outer-window resizing.",
@@ -1944,13 +1947,15 @@ 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(
command, run_game_window(
arguments.window_title, command,
borderless=arguments.borderless, arguments.window_title,
allow_window_resizing=arguments.allow_window_resizing, borderless=arguments.borderless,
gamescope_scaling=arguments.gamescope_scaling, allow_window_resizing=arguments.allow_window_resizing,
suppress_wm_ping=arguments.suppress_wm_ping, gamescope_scaling=arguments.gamescope_scaling,
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)

View File

@@ -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
@@ -396,23 +541,21 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
) )
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
print( print(
f"Warning: Gamescope resize tuning failed: {exc}; " f"Warning: Gamescope resize tuning failed: {exc}; "
"continuing normally.", "continuing normally.",
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)

View File

@@ -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,
) )