Added fallbacks for fsync issues on CachyOS.

This commit is contained in:
2026-08-27 22:02:31 +02:00
parent 4288036bb3
commit e515080171
5 changed files with 83 additions and 3 deletions

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"] __all__ = ["AppPaths", "__version__"]
__version__ = "0.5.2" __version__ = "0.5.3"

View File

@@ -300,6 +300,7 @@ def _login_diagnostics(paths: AppPaths, timeout_seconds: float, as_json: bool) -
probe = report.login_server probe = report.login_server
print(f"Launcher: {report.launcher_version} ({report.launcher_module})") print(f"Launcher: {report.launcher_version} ({report.launcher_module})")
print(f"Python: {report.python_executable}") print(f"Python: {report.python_executable}")
print(f"Host: {report.distribution_id} ({report.kernel_release})")
status = "REACHABLE" if probe.reachable else "UNREACHABLE" status = "REACHABLE" if probe.reachable else "UNREACHABLE"
latency = f", {probe.latency_ms:.1f} ms" if probe.latency_ms is not None else "" latency = f", {probe.latency_ms:.1f} ms" if probe.latency_ms is not None else ""
print(f"Login server: {status} ({probe.host}:{probe.port}{latency})") print(f"Login server: {status} ({probe.host}:{probe.port}{latency})")
@@ -317,6 +318,11 @@ def _login_diagnostics(paths: AppPaths, timeout_seconds: float, as_json: bool) -
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})") print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
else: else:
print(f"Runtime: unavailable ({report.runtime_error or 'selection failed'})") print(f"Runtime: unavailable ({report.runtime_error or 'selection failed'})")
if report.proton_sync_effective is not None:
print(
"Proton sync: "
f"{report.proton_sync_configured} -> {report.proton_sync_effective}"
)
prefix_status = "ready" if report.prefix_ready else "not ready" prefix_status = "ready" if report.prefix_ready else "not ready"
directx_status = "ready" if report.directx_ready else "not ready" directx_status = "ready" if report.directx_ready else "not ready"
print(f"Prefix: {prefix_status}; DirectX helpers {directx_status}") print(f"Prefix: {prefix_status}; DirectX helpers {directx_status}")

View File

@@ -7,6 +7,7 @@ from datetime import datetime
import json import json
import os import os
from pathlib import Path from pathlib import Path
import platform
import shutil import shutil
import sys import sys
from typing import Mapping from typing import Mapping
@@ -25,6 +26,8 @@ WINETRICKS_CODEPAGE_PROBE = (
'winetricks_early_wine_arch cmd.exe /c "chcp 65001 > nul & echo %$1%"' 'winetricks_early_wine_arch cmd.exe /c "chcp 65001 > nul & echo %$1%"'
) )
WINETRICKS_PROTON7_PROBE = 'winetricks_early_wine_arch cmd.exe /c "echo %$1%"' WINETRICKS_PROTON7_PROBE = 'winetricks_early_wine_arch cmd.exe /c "echo %$1%"'
RUNTIME_DEFAULT_SYNC = "runtime-default"
CACHYOS_MARKER = "cachyos"
class RuntimeErrorPT(RuntimeError): class RuntimeErrorPT(RuntimeError):
@@ -238,7 +241,11 @@ def run_game(
WINDOWS_GAME_EXE, WINDOWS_GAME_EXE,
"PTEU_REBORN", "PTEU_REBORN",
] ]
environment = _proton_environment(paths, selection) environment = _proton_environment(
paths,
selection,
synchronization=resolve_proton_sync_mode(settings.proton_sync),
)
if use_gamebox: if use_gamebox:
game_command = build_gamebox_command(game_command, settings) game_command = build_gamebox_command(game_command, settings)
environment["PYTHONPATH"] = _runtime_python_path( environment["PYTHONPATH"] = _runtime_python_path(
@@ -274,10 +281,12 @@ def _umu_environment(paths: AppPaths) -> dict[str, str]:
def _proton_environment( def _proton_environment(
paths: AppPaths, paths: AppPaths,
selection: RuntimeSelection, selection: RuntimeSelection,
*,
synchronization: str = RUNTIME_DEFAULT_SYNC,
) -> dict[str, str]: ) -> dict[str, str]:
if not selection.steam_root: if not selection.steam_root:
raise RuntimeErrorPT("Steam root is missing for Proton") raise RuntimeErrorPT("Steam root is missing for Proton")
return { environment = {
"STEAM_COMPAT_CLIENT_INSTALL_PATH": selection.steam_root, "STEAM_COMPAT_CLIENT_INSTALL_PATH": selection.steam_root,
"STEAM_COMPAT_DATA_PATH": str(paths.compat_data), "STEAM_COMPAT_DATA_PATH": str(paths.compat_data),
"STEAM_COMPAT_INSTALL_PATH": str(paths.runtime_view), "STEAM_COMPAT_INSTALL_PATH": str(paths.runtime_view),
@@ -288,6 +297,57 @@ def _proton_environment(
"PROTON_LOG": "0", "PROTON_LOG": "0",
"DXVK_STATE_CACHE_PATH": str(paths.dxvk_cache), "DXVK_STATE_CACHE_PATH": str(paths.dxvk_cache),
} }
environment.update(_proton_sync_environment(synchronization))
return environment
def detect_distribution_id() -> str:
"""Return the freedesktop distribution ID without invoking distro tools."""
try:
value = platform.freedesktop_os_release().get("ID", "")
except OSError:
return ""
return value.strip().casefold()
def resolve_proton_sync_mode(
configured: str,
*,
distribution_id: str | None = None,
kernel_release: str | None = None,
) -> str:
"""Resolve the configured mode, applying the narrow CachyOS compatibility fallback."""
if configured not in {"auto", "fsync", "esync"}:
raise ValueError(f"unsupported Proton synchronization mode: {configured}")
if configured != "auto":
return configured
distribution = detect_distribution_id() if distribution_id is None else distribution_id
kernel = platform.release() if kernel_release is None else kernel_release
if CACHYOS_MARKER in distribution.casefold() or CACHYOS_MARKER in kernel.casefold():
return "esync"
return RUNTIME_DEFAULT_SYNC
def _proton_sync_environment(synchronization: str) -> dict[str, str]:
if synchronization == RUNTIME_DEFAULT_SYNC:
return {}
if synchronization == "esync":
return {
"PROTON_NO_FSYNC": "1",
"WINEFSYNC": "0",
"PROTON_NO_ESYNC": "0",
"WINEESYNC": "1",
}
if synchronization == "fsync":
return {
"PROTON_NO_FSYNC": "0",
"WINEFSYNC": "1",
"PROTON_NO_ESYNC": "0",
"WINEESYNC": "1",
}
raise ValueError(f"unsupported resolved Proton synchronization mode: {synchronization}")
def _ensure_proton_game_link(paths: AppPaths) -> None: def _ensure_proton_game_link(paths: AppPaths) -> None:

View File

@@ -22,6 +22,7 @@ class Settings:
scaler: str = "fit" scaler: str = "fit"
filter: str = "linear" filter: str = "linear"
runtime: str = "auto" runtime: str = "auto"
proton_sync: str = "auto"
proton_path: str | None = None proton_path: str | None = None
game_source: str | None = None game_source: str | None = None
@@ -50,6 +51,7 @@ def load_settings(paths: AppPaths) -> Settings:
scaler=_string(value, "scaler", "fit"), scaler=_string(value, "scaler", "fit"),
filter=_string(value, "filter", "linear"), filter=_string(value, "filter", "linear"),
runtime=_string(value, "runtime", "auto"), runtime=_string(value, "runtime", "auto"),
proton_sync=_string(value, "proton_sync", "auto"),
proton_path=_optional_string(value, "proton_path"), proton_path=_optional_string(value, "proton_path"),
game_source=_optional_string(value, "game_source"), game_source=_optional_string(value, "game_source"),
) )
@@ -95,6 +97,8 @@ def _validate(settings: Settings) -> None:
raise ValueError(f"unsupported Gamescope filter: {settings.filter}") raise ValueError(f"unsupported Gamescope filter: {settings.filter}")
if settings.runtime not in {"auto", "proton7", "umu"}: if settings.runtime not in {"auto", "proton7", "umu"}:
raise ValueError(f"unsupported runtime selection: {settings.runtime}") raise ValueError(f"unsupported runtime selection: {settings.runtime}")
if settings.proton_sync not in {"auto", "fsync", "esync"}:
raise ValueError(f"unsupported Proton synchronization mode: {settings.proton_sync}")
def _integer(value: dict[str, object], key: str, default: int) -> int: def _integer(value: dict[str, object], key: str, default: int) -> int:

View File

@@ -1371,6 +1371,7 @@ def run_dashboard(paths: AppPaths) -> None:
scaler_value = tk.StringVar(value=current_settings.scaler) scaler_value = tk.StringVar(value=current_settings.scaler)
filter_value = tk.StringVar(value=current_settings.filter) filter_value = tk.StringVar(value=current_settings.filter)
runtime_value = tk.StringVar(value=current_settings.runtime) runtime_value = tk.StringVar(value=current_settings.runtime)
proton_sync_value = tk.StringVar(value=current_settings.proton_sync)
proton_path_value = tk.StringVar(value=current_settings.proton_path or "") proton_path_value = tk.StringVar(value=current_settings.proton_path or "")
game_source_value = tk.StringVar(value=current_settings.game_source or "") game_source_value = tk.StringVar(value=current_settings.game_source or "")
@@ -1511,6 +1512,7 @@ def run_dashboard(paths: AppPaths) -> None:
scaler=scaler_value.get(), scaler=scaler_value.get(),
filter=filter_value.get(), filter=filter_value.get(),
runtime=runtime_value.get(), runtime=runtime_value.get(),
proton_sync=proton_sync_value.get(),
proton_path=proton_path_value.get().strip() or None, proton_path=proton_path_value.get().strip() or None,
game_source=game_source_value.get().strip() or None, game_source=game_source_value.get().strip() or None,
) )
@@ -1637,6 +1639,14 @@ def run_dashboard(paths: AppPaths) -> None:
runtime_value, runtime_value,
("auto", "proton7", "umu"), ("auto", "proton7", "umu"),
) )
add_choice(
rendering_card,
8,
"Proton synchronization",
"Auto uses esync on CachyOS",
proton_sync_value,
("auto", "fsync", "esync"),
)
overrides_card = ttk.LabelFrame( overrides_card = ttk.LabelFrame(
content, content,