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__"]
__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
print(f"Launcher: {report.launcher_version} ({report.launcher_module})")
print(f"Python: {report.python_executable}")
print(f"Host: {report.distribution_id} ({report.kernel_release})")
status = "REACHABLE" if probe.reachable else "UNREACHABLE"
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})")
@@ -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'})")
else:
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"
directx_status = "ready" if report.directx_ready else "not ready"
print(f"Prefix: {prefix_status}; DirectX helpers {directx_status}")

View File

@@ -7,6 +7,7 @@ from datetime import datetime
import json
import os
from pathlib import Path
import platform
import shutil
import sys
from typing import Mapping
@@ -25,6 +26,8 @@ WINETRICKS_CODEPAGE_PROBE = (
'winetricks_early_wine_arch cmd.exe /c "chcp 65001 > nul & 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):
@@ -238,7 +241,11 @@ def run_game(
WINDOWS_GAME_EXE,
"PTEU_REBORN",
]
environment = _proton_environment(paths, selection)
environment = _proton_environment(
paths,
selection,
synchronization=resolve_proton_sync_mode(settings.proton_sync),
)
if use_gamebox:
game_command = build_gamebox_command(game_command, settings)
environment["PYTHONPATH"] = _runtime_python_path(
@@ -274,10 +281,12 @@ def _umu_environment(paths: AppPaths) -> dict[str, str]:
def _proton_environment(
paths: AppPaths,
selection: RuntimeSelection,
*,
synchronization: str = RUNTIME_DEFAULT_SYNC,
) -> dict[str, str]:
if not selection.steam_root:
raise RuntimeErrorPT("Steam root is missing for Proton")
return {
environment = {
"STEAM_COMPAT_CLIENT_INSTALL_PATH": selection.steam_root,
"STEAM_COMPAT_DATA_PATH": str(paths.compat_data),
"STEAM_COMPAT_INSTALL_PATH": str(paths.runtime_view),
@@ -288,6 +297,57 @@ def _proton_environment(
"PROTON_LOG": "0",
"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:

View File

@@ -22,6 +22,7 @@ class Settings:
scaler: str = "fit"
filter: str = "linear"
runtime: str = "auto"
proton_sync: str = "auto"
proton_path: str | None = None
game_source: str | None = None
@@ -50,6 +51,7 @@ def load_settings(paths: AppPaths) -> Settings:
scaler=_string(value, "scaler", "fit"),
filter=_string(value, "filter", "linear"),
runtime=_string(value, "runtime", "auto"),
proton_sync=_string(value, "proton_sync", "auto"),
proton_path=_optional_string(value, "proton_path"),
game_source=_optional_string(value, "game_source"),
)
@@ -95,6 +97,8 @@ def _validate(settings: Settings) -> None:
raise ValueError(f"unsupported Gamescope filter: {settings.filter}")
if settings.runtime not in {"auto", "proton7", "umu"}:
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:

View File

@@ -1371,6 +1371,7 @@ def run_dashboard(paths: AppPaths) -> None:
scaler_value = tk.StringVar(value=current_settings.scaler)
filter_value = tk.StringVar(value=current_settings.filter)
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 "")
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(),
filter=filter_value.get(),
runtime=runtime_value.get(),
proton_sync=proton_sync_value.get(),
proton_path=proton_path_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,
("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(
content,