Fixed issues with gamescope not properly launching.

This commit is contained in:
2026-09-06 22:57:54 +02:00
parent f53f4d0b08
commit bea9d6fda7
9 changed files with 252 additions and 18 deletions

View File

@@ -162,6 +162,7 @@ if [[ ! -d "${SOURCE_ROOT}/ptlinux" \
|| ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-live-resize.patch" \ || ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-live-resize.patch" \
|| ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-letterbox-backing.patch" \ || ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-letterbox-backing.patch" \
|| ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-libdecor-input-scope.patch" \ || ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-libdecor-input-scope.patch" \
|| ! -f "${SOURCE_ROOT}/ptlinux/native/gamescope-keep-nested-windows-visible.patch" \
|| ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.desktop" \ || ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.desktop" \
|| ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.png" \ || ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.png" \
|| ! -f "${SOURCE_ROOT}/packaging/pristontale-eu" \ || ! -f "${SOURCE_ROOT}/packaging/pristontale-eu" \
@@ -254,6 +255,7 @@ install -m 0644 \
"${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-live-resize.patch" \ "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-live-resize.patch" \
"${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-letterbox-backing.patch" \ "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-letterbox-backing.patch" \
"${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-libdecor-input-scope.patch" \ "${SOURCE_ROOT}/ptlinux/native/gamescope-wayland-libdecor-input-scope.patch" \
"${SOURCE_ROOT}/ptlinux/native/gamescope-keep-nested-windows-visible.patch" \
"${STAGING_RELEASE}/ptlinux/native/" "${STAGING_RELEASE}/ptlinux/native/"
install -m 0644 "${SOURCE_ROOT}/ptdiag/__init__.py" "${STAGING_RELEASE}/ptdiag/__init__.py" install -m 0644 "${SOURCE_ROOT}/ptdiag/__init__.py" "${STAGING_RELEASE}/ptdiag/__init__.py"
install -m 0644 "${SOURCE_ROOT}/ptdiag/assbin.py" "${STAGING_RELEASE}/ptdiag/assbin.py" install -m 0644 "${SOURCE_ROOT}/ptdiag/assbin.py" "${STAGING_RELEASE}/ptdiag/assbin.py"

View File

@@ -50,7 +50,12 @@ from .launcher import (
launch_official_updater, launch_official_updater,
prepare_launcher_binary, prepare_launcher_binary,
) )
from .locking import LauncherPlayLease, launcher_operation_lock, launcher_play_lock from .locking import (
LauncherPlayLease,
launcher_operation_lock,
launcher_play_lock,
managed_game_process_is_active,
)
from .login_diagnostics import inspect_login_environment from .login_diagnostics import inspect_login_environment
from .login_trace import DEFAULT_TRACE_TIMEOUT_SECONDS, LOGIN_TRACE_DISABLED_REASON from .login_trace import DEFAULT_TRACE_TIMEOUT_SECONDS, LOGIN_TRACE_DISABLED_REASON
from .overlay import cache_compatibility_binaries, prepare_runtime_view from .overlay import cache_compatibility_binaries, prepare_runtime_view
@@ -855,6 +860,7 @@ def _play(
realm=realm, realm=realm,
reuse_prepared=not play_lease.preparation_allowed, reuse_prepared=not play_lease.preparation_allowed,
before_game=play_lease.allow_shared_clients, before_game=play_lease.allow_shared_clients,
game_lease_fds=play_lease.inherited_fds,
) )
@@ -869,6 +875,7 @@ def _play_locked(
realm: str | None = None, realm: str | None = None,
reuse_prepared: bool = False, reuse_prepared: bool = False,
before_game: Callable[[], None] | None = None, before_game: Callable[[], None] | None = None,
game_lease_fds: Sequence[int] = (),
) -> int: ) -> int:
if game_dir is None and game_import_requires_verification(paths): if game_dir is None and game_import_requires_verification(paths):
raise LauncherError( raise LauncherError(
@@ -980,6 +987,7 @@ def _play_locked(
use_gamebox=not no_box, use_gamebox=not no_box,
concurrent_prefix_launch=reuse_prepared, concurrent_prefix_launch=reuse_prepared,
check=False, check=False,
pass_fds=game_lease_fds,
) )
except KeyboardInterrupt: except KeyboardInterrupt:
current_game_log = _current_game_log( current_game_log = _current_game_log(
@@ -1505,7 +1513,10 @@ def _play_guard(
) -> AbstractContextManager[LauncherPlayLease]: ) -> AbstractContextManager[LauncherPlayLease]:
if dry_run: if dry_run:
return nullcontext(LauncherPlayLease.dry_run()) return nullcontext(LauncherPlayLease.dry_run())
return launcher_play_lock(paths.launcher_operation_lock) return launcher_play_lock(
paths.launcher_operation_lock,
active_game_probe=lambda: _managed_game_process_is_active(paths),
)
def _operation_guard( def _operation_guard(
@@ -1515,7 +1526,18 @@ def _operation_guard(
) -> AbstractContextManager[None]: ) -> AbstractContextManager[None]:
if dry_run: if dry_run:
return nullcontext() return nullcontext()
return launcher_operation_lock(paths.launcher_operation_lock, operation) return launcher_operation_lock(
paths.launcher_operation_lock,
operation,
active_game_probe=lambda: _managed_game_process_is_active(paths),
)
def _managed_game_process_is_active(paths: AppPaths) -> bool:
return managed_game_process_is_active(
paths.runtime_view,
(paths.proton_prefix, paths.umu_prefix),
)
def _emit(value: object, as_json: bool) -> None: def _emit(value: object, as_json: bool) -> None:

View File

@@ -99,10 +99,14 @@ class CommandRunner:
check: bool = True, check: bool = True,
stream_output: bool = False, stream_output: bool = False,
output_line_filter: OutputLineFilter | None = None, output_line_filter: OutputLineFilter | None = None,
pass_fds: Sequence[int] = (),
) -> CommandOutcome: ) -> CommandOutcome:
arguments = [str(value) for value in argv] arguments = [str(value) for value in argv]
if not arguments: if not arguments:
raise ValueError("command argv cannot be empty") raise ValueError("command argv cannot be empty")
inherited_fds = tuple(dict.fromkeys(pass_fds))
if any(not isinstance(value, int) or value < 0 for value in inherited_fds):
raise ValueError("inherited file descriptors must be non-negative integers")
overrides = dict(env_overrides or {}) overrides = dict(env_overrides or {})
started = time.monotonic() started = time.monotonic()
if self.dry_run: if self.dry_run:
@@ -128,6 +132,7 @@ class CommandRunner:
timeout_seconds=timeout_seconds, timeout_seconds=timeout_seconds,
started=started, started=started,
output_line_filter=output_line_filter, output_line_filter=output_line_filter,
pass_fds=inherited_fds,
) )
else: else:
try: try:
@@ -142,6 +147,7 @@ class CommandRunner:
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=timeout_seconds, timeout=timeout_seconds,
check=False, check=False,
pass_fds=inherited_fds,
) )
outcome = CommandOutcome( outcome = CommandOutcome(
argv=arguments, argv=arguments,
@@ -176,6 +182,7 @@ class CommandRunner:
timeout_seconds: float, timeout_seconds: float,
started: float, started: float,
output_line_filter: OutputLineFilter | None, output_line_filter: OutputLineFilter | None,
pass_fds: tuple[int, ...],
) -> CommandOutcome: ) -> CommandOutcome:
process = subprocess.Popen( process = subprocess.Popen(
arguments, arguments,
@@ -188,6 +195,7 @@ class CommandRunner:
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
bufsize=1, bufsize=1,
start_new_session=True, start_new_session=True,
pass_fds=pass_fds,
) )
stdout = BoundedTextCapture() stdout = BoundedTextCapture()
stderr = BoundedTextCapture() stderr = BoundedTextCapture()

View File

@@ -43,6 +43,8 @@ _X_PROPERTY_NOTIFY = 28
_X_PROPERTY_CHANGE_MASK = 1 << 22 _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_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
@@ -448,6 +450,18 @@ class _X11:
return None return None
return values[0] return values[0]
def restore_normal_window_state(self, window: int) -> bool:
"""Undo a nested iconify request without raising the host window."""
self._replace_long_property(
window,
b"WM_STATE",
b"WM_STATE",
(_X_WM_STATE_NORMAL, 0),
)
self.library.XSync(self.display, 0)
return self.window_state(window) == _X_WM_STATE_NORMAL
def window_is_borderless(self, window: int) -> bool | None: def window_is_borderless(self, window: int) -> bool | None:
"""Return whether the client currently requests no Motif decorations.""" """Return whether the client currently requests no Motif decorations."""
@@ -1463,6 +1477,54 @@ def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
return False return False
def _resolve_gamescope_game_window(
mapper: _X11,
window: int,
*,
window_title: str,
excluded_window_ids: Collection[int],
) -> int | None:
"""Return a valid nested client across Wine's login-window transition."""
try:
state = mapper.window_state(window)
except Exception:
state = None
if state == _X_WM_STATE_ICONIC:
try:
if not mapper.restore_normal_window_state(window):
return None
except Exception:
return None
print(
"Game login window requested minimization; kept it visible inside "
"Gamescope.",
file=sys.stderr,
flush=True,
)
return window
if state is not None and state != _X_WM_STATE_WITHDRAWN:
return window
try:
found = mapper.find_first_window(
window_title,
excluded_window_ids=set(excluded_window_ids) | {window},
)
except Exception:
return None
if found is None:
return None
replacement, title = found
_apply_game_window_identity(mapper, replacement)
print(
f"Game login window changed; following it inside Gamescope: {title}",
file=sys.stderr,
flush=True,
)
return replacement
def _enable_game_window_scaling(mapper: _X11, window: int) -> bool: def _enable_game_window_scaling(mapper: _X11, window: int) -> bool:
"""Enable confirmed resize/maximize actions without risking game startup.""" """Enable confirmed resize/maximize actions without risking game startup."""
@@ -1587,9 +1649,14 @@ def _maintain_gamescope_game_window_focus(
mapper: _X11, mapper: _X11,
process: subprocess.Popen[bytes], process: subprocess.Popen[bytes],
window: int, window: int,
*,
window_title: str = WINDOW_TITLE,
existing_window_ids: Collection[int] = (),
) -> int: ) -> int:
"""Restore nested keyboard focus after host resize/maximize operations.""" """Keep a valid, focused nested client through login and host resizing."""
active_window: int = window
retired_window_ids = set(existing_window_ids)
next_focus_repair_at = ( next_focus_repair_at = (
time.monotonic() + GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS time.monotonic() + GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS
) )
@@ -1598,7 +1665,22 @@ def _maintain_gamescope_game_window_focus(
now = time.monotonic() now = time.monotonic()
if now < next_focus_repair_at: if now < next_focus_repair_at:
continue continue
_repair_gamescope_game_window_focus(mapper, window) resolved_window = _resolve_gamescope_game_window(
mapper,
active_window,
window_title=window_title,
excluded_window_ids=retired_window_ids,
)
if resolved_window is None:
next_focus_repair_at = (
time.monotonic() + GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS
)
continue
resolved_window_id = resolved_window
if resolved_window_id != active_window:
retired_window_ids.add(active_window)
active_window = resolved_window_id
_repair_gamescope_game_window_focus(mapper, active_window)
next_focus_repair_at = ( next_focus_repair_at = (
time.monotonic() + GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS time.monotonic() + GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS
) )
@@ -1785,6 +1867,8 @@ def run_game_window(
mapper, mapper,
process, process,
game_window, game_window,
window_title=window_title,
existing_window_ids=existing_window_ids,
) )
_prepare_game_window( _prepare_game_window(
mapper, mapper,

View File

@@ -27,10 +27,11 @@ WAYLAND_BACKING_FIX_VERSION = (3, 16, 28)
BUNDLED_GAMESCOPE_VERSION = (3, 16, 20) BUNDLED_GAMESCOPE_VERSION = (3, 16, 20)
BUNDLED_GAMESCOPE_FILENAME = "gamescope-3.16.20-ubuntu26.04-amd64" BUNDLED_GAMESCOPE_FILENAME = "gamescope-3.16.20-ubuntu26.04-amd64"
BUNDLED_GAMESCOPE_SHA256 = ( BUNDLED_GAMESCOPE_SHA256 = (
"d4a082d8db0c467f65b429b6fb339b56b9a0d3e17e9c1957ede9ba1cb9a3ec90" "3fc81536c22dcce2c593c11507a612c13221f5b2ce2e5326f7c63706ca965639"
) )
BUNDLED_GAMESCOPE_OS_ID = "ubuntu" BUNDLED_GAMESCOPE_OS_ID = "ubuntu"
BUNDLED_GAMESCOPE_OS_VERSION = "26.04" BUNDLED_GAMESCOPE_OS_VERSION = "26.04"
KEEP_NESTED_WINDOWS_VISIBLE_ENV = "PTLINUX_GAMESCOPE_KEEP_WINDOWS_VISIBLE"
HASH_READ_SIZE = 256 * 1024 HASH_READ_SIZE = 256 * 1024
_GAMESCOPE_VERSION_PATTERN = re.compile( _GAMESCOPE_VERSION_PATTERN = re.compile(
r"\bgamescope\s+version\s+(\d+)\.(\d+)\.(\d+)", r"\bgamescope\s+version\s+(\d+)\.(\d+)\.(\d+)",
@@ -357,6 +358,11 @@ 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 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
# visible without activating or raising the host window.
environment[KEEP_NESTED_WINDOWS_VISIBLE_ENV] = "1"
try: try:
process = subprocess.Popen(launch_command, env=environment) process = subprocess.Popen(launch_command, env=environment)
except OSError as exc: except OSError as exc:
@@ -374,7 +380,7 @@ def run_gamescope(command: Sequence[str], width: int, height: int) -> int:
if using_bundled_gamescope: if using_bundled_gamescope:
message += ( message += (
" Launcher-local lazy-output, live-resize, letterbox backing, " " Launcher-local lazy-output, live-resize, letterbox backing, "
"and decoration-input fixes are active." "decoration-input, and login-window visibility fixes are active."
) )
print(message, file=sys.stderr, flush=True) print(message, file=sys.stderr, flush=True)
elif backend == "wayland": elif backend == "wayland":

View File

@@ -7,7 +7,12 @@ from datetime import datetime
import fcntl import fcntl
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterator, TextIO from typing import Callable, Iterator, Sequence, TextIO
ActiveGameProbe = Callable[[], bool]
MAX_PROCESS_ENVIRONMENT_BYTES = 1024 * 1024
MAX_PROCESS_COMMAND_BYTES = 128 * 1024
class OperationBusyError(RuntimeError): class OperationBusyError(RuntimeError):
@@ -51,9 +56,22 @@ class LauncherPlayLease:
def record_owned(self) -> bool: def record_owned(self) -> bool:
return self._record_owned return self._record_owned
@property
def inherited_fds(self) -> tuple[int, ...]:
"""Keep the shared lease alive in the launched game supervisor."""
if self._handle is None:
return ()
return (self._handle.fileno(),)
@contextmanager @contextmanager
def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]: def launcher_operation_lock(
lock_path: Path,
operation: str,
*,
active_game_probe: ActiveGameProbe | None = None,
) -> Iterator[None]:
lock_path.parent.mkdir(parents=True, exist_ok=True) lock_path.parent.mkdir(parents=True, exist_ok=True)
with lock_path.open("a+", encoding="utf-8") as handle: with lock_path.open("a+", encoding="utf-8") as handle:
try: try:
@@ -64,6 +82,11 @@ def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]:
raise OperationBusyError( raise OperationBusyError(
f"launcher files are busy ({owner}); wait for it to finish and try again" f"launcher files are busy ({owner}); wait for it to finish and try again"
) from exc ) from exc
if active_game_probe is not None and active_game_probe():
raise OperationBusyError(
"launcher files are busy (active managed game client detected); "
"close every game client and try again"
)
_write_owner(handle, operation) _write_owner(handle, operation)
try: try:
yield yield
@@ -78,6 +101,8 @@ def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]:
def launcher_play_lock( def launcher_play_lock(
lock_path: Path, lock_path: Path,
operation: str = "play", operation: str = "play",
*,
active_game_probe: ActiveGameProbe | None = None,
) -> Iterator[LauncherPlayLease]: ) -> Iterator[LauncherPlayLease]:
"""Serialize preparation, then allow multiple active game clients. """Serialize preparation, then allow multiple active game clients.
@@ -104,6 +129,18 @@ def launcher_play_lock(
preparation_allowed=False, preparation_allowed=False,
record_owned=False, record_owned=False,
) )
else:
if active_game_probe is not None and active_game_probe():
# A game from an older launcher may have outlived its Python
# supervisor and therefore its inherited lock. Rejoin that live
# prefix without mutating its runtime view or waiting for it to exit.
_write_owner(handle, operation)
fcntl.flock(handle.fileno(), fcntl.LOCK_SH)
lease = LauncherPlayLease(
handle,
preparation_allowed=False,
record_owned=True,
)
else: else:
_write_owner(handle, f"{operation} preparation") _write_owner(handle, f"{operation} preparation")
lease = LauncherPlayLease( lease = LauncherPlayLease(
@@ -123,6 +160,79 @@ def launcher_play_lock(
fcntl.flock(handle.fileno(), fcntl.LOCK_UN) fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def managed_game_process_is_active(
runtime_view: Path,
wine_prefixes: Sequence[Path],
*,
process_root: Path = Path("/proc"),
) -> bool:
"""Detect a managed Wine game that outlived its launcher supervisor."""
prefixes = {_normalized_path(prefix) for prefix in wine_prefixes}
prefixes.discard("")
if not prefixes:
return False
runtime_marker = _normalized_path(runtime_view).casefold().replace("\\", "/")
if not runtime_marker:
return False
runtime_marker = runtime_marker.rstrip("/") + "/"
try:
processes = tuple(process_root.iterdir())
except OSError:
return False
for process in processes:
if not process.name.isdecimal():
continue
try:
name = (process / "comm").read_text(
encoding="utf-8",
errors="replace",
).strip().casefold()
except OSError:
continue
if not name.endswith(".exe"):
continue
environment = _read_bounded(process / "environ", MAX_PROCESS_ENVIRONMENT_BYTES)
if environment is None:
continue
wine_prefix = next(
(
entry.removeprefix(b"WINEPREFIX=")
for entry in environment.split(b"\0")
if entry.startswith(b"WINEPREFIX=")
),
None,
)
if (
wine_prefix is None
or _normalized_path(os.fsdecode(wine_prefix)) not in prefixes
):
continue
command = _read_bounded(process / "cmdline", MAX_PROCESS_COMMAND_BYTES)
if command is None:
continue
normalized_command = os.fsdecode(command).casefold().replace("\\", "/")
if runtime_marker in normalized_command:
return True
return False
def _read_bounded(path: Path, maximum_bytes: int) -> bytes | None:
try:
with path.open("rb") as handle:
value = handle.read(maximum_bytes + 1)
except OSError:
return None
return value if len(value) <= maximum_bytes else None
def _normalized_path(path: Path | str) -> str:
value = str(path)
if not value:
return ""
return str(Path(value).expanduser().resolve(strict=False))
def _read_owner(handle: TextIO) -> str: def _read_owner(handle: TextIO) -> str:
handle.seek(0) handle.seek(0)
return handle.read().strip() or "unknown operation" return handle.read().strip() or "unknown operation"

View File

@@ -406,6 +406,7 @@ def run_game(
check: bool = True, check: bool = True,
extra_environment: Mapping[str, str] | None = None, extra_environment: Mapping[str, str] | None = None,
output_line_filter: OutputLineFilter | None = None, output_line_filter: OutputLineFilter | None = None,
pass_fds: Sequence[int] = (),
) -> CommandOutcome: ) -> CommandOutcome:
_require_container_host(selection, runner) _require_container_host(selection, runner)
if not paths.runtime_view.is_dir() and not runner.dry_run: if not paths.runtime_view.is_dir() and not runner.dry_run:
@@ -502,6 +503,7 @@ def run_game(
check=check, check=check,
stream_output=True, stream_output=True,
output_line_filter=output_line_filter, output_line_filter=output_line_filter,
pass_fds=pass_fds,
) )

View File

@@ -1,11 +1,11 @@
{ {
"archive_sha256": "890443aab4cd0e33bb2e2a44ea86b3fd929488eba045b803c2c0662ce68c08ff", "archive_sha256": "3e00fc6cd72b5ae79adee56642a10b90d410080e67da099292aefcd5cf406681",
"archive_size": 1410277, "archive_size": 1413002,
"archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v0.9.5/pristontale-eu-linux-update-v0.9.5.tar.gz", "archive_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/download/v0.9.5/pristontale-eu-linux-update-v0.9.5.tar.gz",
"channel": "stable", "channel": "stable",
"minimum_updater_version": "0.5.0", "minimum_updater_version": "0.5.0",
"notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v0.9.5", "notes_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux/releases/tag/v0.9.5",
"published_at": "2026-09-06T20:06:31Z", "published_at": "2026-09-06T20:09:16Z",
"schema": 1, "schema": 1,
"version": "0.9.5" "version": "0.9.5"
} }

Binary file not shown.