Optimizations and performance boost to pre-loader for launcher and game client.

This commit is contained in:
2026-09-01 22:21:54 +02:00
parent c2400db19e
commit 15dbd90dbb
3 changed files with 370 additions and 31 deletions

View File

@@ -1,4 +1,28 @@
from .cli import main from __future__ import annotations
import sys
def main() -> int:
"""Dispatch the common GUI path without importing every CLI operation."""
arguments = sys.argv[1:]
if not arguments or arguments == ["gui"]:
from .paths import AppPaths
from .ui import run_dashboard
try:
run_dashboard(AppPaths.from_environment())
except KeyboardInterrupt:
return 130
except (ValueError, OSError, RuntimeError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
return 0
from .cli import main as cli_main
return cli_main(arguments)
raise SystemExit(main()) raise SystemExit(main())

View File

@@ -44,15 +44,46 @@ PATCHED_V21_GAME_DLL_SHA256 = (
RETIRED_POST_AUTH_GAME_DLL_SHA256 = ( RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd" "93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
) )
RUNTIME_VIEW_CONFIGURATION_VERSION = 1 RUNTIME_VIEW_CONFIGURATION_VERSION = 2
class OverlayError(RuntimeError): class OverlayError(RuntimeError):
"""Raised when a safe runtime view cannot be built.""" """Raised when a safe runtime view cannot be built."""
@dataclass(frozen=True)
class RuntimeFileIdentity:
"""Cheap change detector for one previously hash-verified runtime input."""
name: str
device: int
inode: int
mode: int
size: int
mtime_ns: int
ctime_ns: int
@dataclass(frozen=True) @dataclass(frozen=True)
class OverlayResult: class OverlayResult:
source_game: str
runtime_view: str
source_scene_sha256: str
runtime_scene_sha256: str
scene_action: str
binary_action: str
game_exe_sha256: str
game_dll_sha256: str
input_identities: tuple[RuntimeFileIdentity, ...]
runtime_scene_identity: RuntimeFileIdentity
created_at: str
links_created: int
@dataclass(frozen=True)
class LegacyOverlayResult:
"""Version-one runtime manifest retained only for a one-time fast migration."""
source_game: str source_game: str
runtime_view: str runtime_view: str
source_scene_sha256: str source_scene_sha256: str
@@ -94,8 +125,9 @@ def inspect_runtime_compatibility(
This preflight is intentionally read-only: it never normalizes a scene, This preflight is intentionally read-only: it never normalizes a scene,
builds the verified runtime DLL, prepares save data, or replaces the runtime builds the verified runtime DLL, prepares save data, or replaces the runtime
view. The normal preparation path still performs every authoritative check view. Preparation reuses a prior authoritative result only while every input
immediately before launch. identity and runtime-view link remains unchanged; otherwise it repeats the
full checks before launch.
""" """
requested_source = source_game.expanduser() requested_source = source_game.expanduser()
@@ -105,6 +137,9 @@ def inspect_runtime_compatibility(
try: try:
source = requested_source.resolve(strict=True) source = requested_source.resolve(strict=True)
_validate_game_tree(source) _validate_game_tree(source)
cached = _cached_runtime_compatibility(paths, source)
if cached is not None:
return cached
source_scene = source / WHISPERING_VALE_RELATIVE source_scene = source / WHISPERING_VALE_RELATIVE
inspection = inspect_wrapped_assbin(source_scene) inspection = inspect_wrapped_assbin(source_scene)
source_scene_sha256 = inspection.file_sha256 source_scene_sha256 = inspection.file_sha256
@@ -139,10 +174,14 @@ def prepare_runtime_view(
source = source_game.expanduser().resolve(strict=True) source = source_game.expanduser().resolve(strict=True)
_validate_game_tree(source) _validate_game_tree(source)
_validate_runtime_target(paths) _validate_runtime_target(paths)
game_save = _prepare_persistent_save(paths, source)
reused = _reuse_runtime_view(paths, source, game_save)
if reused is not None:
return reused
input_identities = _runtime_input_identities(paths, source)
source_scene = source / WHISPERING_VALE_RELATIVE source_scene = source / WHISPERING_VALE_RELATIVE
inspection = inspect_wrapped_assbin(source_scene) inspection = inspect_wrapped_assbin(source_scene)
action = _scene_action(inspection) action = _scene_action(inspection)
game_save = _prepare_persistent_save(paths, source)
( (
binary_action, binary_action,
game_exe_source, game_exe_source,
@@ -152,7 +191,7 @@ def prepare_runtime_view(
paths, paths,
source, source,
) )
reused = _reuse_runtime_view( legacy_reuse = _reuse_legacy_runtime_view(
paths, paths,
source, source,
inspection, inspection,
@@ -162,9 +201,10 @@ def prepare_runtime_view(
game_exe_source, game_exe_source,
game_dll_source, game_dll_source,
source_binary_hashes, source_binary_hashes,
input_identities,
) )
if reused is not None: if legacy_reuse is not None:
return reused return legacy_reuse
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}" build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
backup = paths.data / f".{paths.runtime_view.name}.old-{uuid.uuid4().hex}" backup = paths.data / f".{paths.runtime_view.name}.old-{uuid.uuid4().hex}"
@@ -213,6 +253,10 @@ def prepare_runtime_view(
validate_scene_normalization(normalization) validate_scene_normalization(normalization)
else: else:
shutil.copy2(source_scene, runtime_scene) shutil.copy2(source_scene, runtime_scene)
if _runtime_input_identities(paths, source) != input_identities:
raise OverlayError(
"game files changed while the managed runtime view was being prepared"
)
if paths.runtime_view.exists() or paths.runtime_view.is_symlink(): if paths.runtime_view.exists() or paths.runtime_view.is_symlink():
paths.runtime_view.replace(backup) paths.runtime_view.replace(backup)
@@ -234,6 +278,11 @@ def prepare_runtime_view(
binary_action=binary_action, binary_action=binary_action,
game_exe_sha256=_sha256(paths.runtime_view / "Game.exe"), game_exe_sha256=_sha256(paths.runtime_view / "Game.exe"),
game_dll_sha256=_sha256(paths.runtime_view / "game.dll"), game_dll_sha256=_sha256(paths.runtime_view / "game.dll"),
input_identities=input_identities,
runtime_scene_identity=_runtime_file_identity(
"runtime-scene",
paths.runtime_view / WHISPERING_VALE_RELATIVE,
),
created_at=datetime.now().astimezone().isoformat(), created_at=datetime.now().astimezone().isoformat(),
links_created=links_created, links_created=links_created,
) )
@@ -470,7 +519,99 @@ def _reject_save_symlinks(root: Path) -> None:
raise OverlayError(f"persistent game save contains a symbolic link: {path}") raise OverlayError(f"persistent game save contains a symbolic link: {path}")
def _reuse_runtime_view( def _runtime_file_identity(
name: str,
path: Path,
*,
follow_symlinks: bool = True,
) -> RuntimeFileIdentity:
state = path.stat(follow_symlinks=follow_symlinks)
return RuntimeFileIdentity(
name=name,
device=state.st_dev,
inode=state.st_ino,
mode=state.st_mode,
size=state.st_size,
mtime_ns=state.st_mtime_ns,
ctime_ns=state.st_ctime_ns,
)
def _runtime_input_identities(
paths: AppPaths,
source: Path,
) -> tuple[RuntimeFileIdentity, ...]:
"""Snapshot inputs whose hashes established the cached runtime profile.
Nanosecond mtime plus ctime/inode/device prevents ordinary edits, replacements,
or timestamp restoration from bypassing the next authoritative hash check.
"""
identities = [
_runtime_file_identity("source-game-exe", source / "Game.exe"),
_runtime_file_identity("source-game-dll", source / "game.dll"),
_runtime_file_identity("source-scene", source / WHISPERING_VALE_RELATIVE),
]
cache = paths.compatibility_binaries
if cache.exists() or cache.is_symlink():
identities.append(
_runtime_file_identity(
"compatibility-cache",
cache,
follow_symlinks=False,
)
)
if cache.is_dir():
for name in COMPATIBILITY_BINARY_HASHES:
path = cache / name
if path.exists() or path.is_symlink():
identities.append(
_runtime_file_identity(
f"compatibility-cache-{name.casefold()}",
path,
)
)
return tuple(identities)
def _matching_cached_runtime_result(
paths: AppPaths,
source: Path,
) -> OverlayResult | None:
result = _read_overlay_result(paths.runtime_manifest)
if result is None:
return None
if result.source_game != str(source):
return None
if result.runtime_view != str(paths.runtime_view):
return None
try:
current_identities = _runtime_input_identities(paths, source)
except OSError:
return None
if current_identities != result.input_identities:
return None
return result
def _cached_runtime_compatibility(
paths: AppPaths,
source: Path,
) -> RuntimeCompatibility | None:
result = _matching_cached_runtime_result(paths, source)
if result is None:
return None
return RuntimeCompatibility(
compatible=True,
source_game=str(source),
source_scene_sha256=result.source_scene_sha256,
scene_action=result.scene_action,
binary_action=result.binary_action,
reason=None,
)
def _reuse_legacy_runtime_view(
paths: AppPaths, paths: AppPaths,
source: Path, source: Path,
inspection: SceneInspection, inspection: SceneInspection,
@@ -480,22 +621,82 @@ def _reuse_runtime_view(
game_exe_source: Path, game_exe_source: Path,
game_dll_source: Path, game_dll_source: Path,
source_binary_hashes: Mapping[str, str], source_binary_hashes: Mapping[str, str],
input_identities: tuple[RuntimeFileIdentity, ...],
) -> OverlayResult | None: ) -> OverlayResult | None:
result = _read_overlay_result(paths.runtime_manifest) """Upgrade an unchanged v1 view after its original hash checks pass once."""
legacy = _read_legacy_overlay_result(paths.runtime_manifest)
if legacy is None:
return None
if legacy.source_game != str(source):
return None
if legacy.runtime_view != str(paths.runtime_view):
return None
if legacy.source_scene_sha256 != inspection.file_sha256:
return None
if legacy.scene_action != scene_action or legacy.binary_action != binary_action:
return None
if legacy.game_exe_sha256 != source_binary_hashes.get("Game.exe"):
return None
if legacy.game_dll_sha256 != source_binary_hashes.get("game.dll"):
return None
if not _runtime_layout_ready(
paths,
source,
game_save,
game_exe_source,
game_dll_source,
legacy.links_created,
):
return None
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
try:
if _sha256(runtime_scene) != legacy.runtime_scene_sha256:
return None
if _runtime_input_identities(paths, source) != input_identities:
raise OverlayError(
"game files changed while the legacy runtime view was being verified"
)
runtime_scene_identity = _runtime_file_identity(
"runtime-scene",
runtime_scene,
)
except OSError:
return None
result = OverlayResult(
source_game=legacy.source_game,
runtime_view=legacy.runtime_view,
source_scene_sha256=legacy.source_scene_sha256,
runtime_scene_sha256=legacy.runtime_scene_sha256,
scene_action=legacy.scene_action,
binary_action=legacy.binary_action,
game_exe_sha256=legacy.game_exe_sha256,
game_dll_sha256=legacy.game_dll_sha256,
input_identities=input_identities,
runtime_scene_identity=runtime_scene_identity,
created_at=legacy.created_at,
links_created=legacy.links_created,
)
_write_json(
paths.runtime_manifest,
{
"configuration_version": RUNTIME_VIEW_CONFIGURATION_VERSION,
**asdict(result),
},
)
return result
def _reuse_runtime_view(
paths: AppPaths,
source: Path,
game_save: Path,
) -> OverlayResult | None:
result = _matching_cached_runtime_result(paths, source)
if result is None: if result is None:
return None return None
if result.source_game != str(source): game_exe_source = source / "Game.exe"
return None game_dll_source = source / "game.dll"
if result.runtime_view != str(paths.runtime_view):
return None
if result.source_scene_sha256 != inspection.file_sha256:
return None
if result.scene_action != scene_action or result.binary_action != binary_action:
return None
if result.game_exe_sha256 != source_binary_hashes.get("Game.exe"):
return None
if result.game_dll_sha256 != source_binary_hashes.get("game.dll"):
return None
if not _runtime_layout_ready( if not _runtime_layout_ready(
paths, paths,
source, source,
@@ -507,7 +708,12 @@ def _reuse_runtime_view(
return None return None
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
try: try:
if _sha256(runtime_scene) != result.runtime_scene_sha256: if (
_runtime_file_identity("runtime-scene", runtime_scene)
!= result.runtime_scene_identity
):
return None
if _runtime_input_identities(paths, source) != result.input_identities:
return None return None
except OSError: except OSError:
return None return None
@@ -651,7 +857,101 @@ def _read_overlay_result(path: Path) -> OverlayResult | None:
return None return None
if links_created < 0: if links_created < 0:
return None return None
input_values = value.get("input_identities")
if not isinstance(input_values, list) or not input_values:
return None
input_identities: list[RuntimeFileIdentity] = []
for input_value in input_values:
identity = _parse_runtime_file_identity(input_value)
if identity is None:
return None
input_identities.append(identity)
input_names = {identity.name for identity in input_identities}
if len(input_names) != len(input_identities) or not {
"source-game-exe",
"source-game-dll",
"source-scene",
}.issubset(input_names):
return None
runtime_scene_identity = _parse_runtime_file_identity(
value.get("runtime_scene_identity")
)
if (
runtime_scene_identity is None
or runtime_scene_identity.name != "runtime-scene"
):
return None
return OverlayResult( return OverlayResult(
source_game=value["source_game"],
runtime_view=value["runtime_view"],
source_scene_sha256=value["source_scene_sha256"],
runtime_scene_sha256=value["runtime_scene_sha256"],
scene_action=value["scene_action"],
binary_action=value["binary_action"],
game_exe_sha256=value["game_exe_sha256"],
game_dll_sha256=value["game_dll_sha256"],
input_identities=tuple(input_identities),
runtime_scene_identity=runtime_scene_identity,
created_at=value["created_at"],
links_created=links_created,
)
def _parse_runtime_file_identity(value: object) -> RuntimeFileIdentity | None:
if not isinstance(value, dict):
return None
name = value.get("name")
if not isinstance(name, str) or not name:
return None
fields: dict[str, int] = {}
for field in ("device", "inode", "mode", "size", "mtime_ns", "ctime_ns"):
field_value = value.get(field)
if (
isinstance(field_value, bool)
or not isinstance(field_value, int)
or field_value < 0
):
return None
fields[field] = field_value
return RuntimeFileIdentity(
name=name,
device=fields["device"],
inode=fields["inode"],
mode=fields["mode"],
size=fields["size"],
mtime_ns=fields["mtime_ns"],
ctime_ns=fields["ctime_ns"],
)
def _read_legacy_overlay_result(path: Path) -> LegacyOverlayResult | None:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return None
if not isinstance(value, dict) or value.get("configuration_version") != 1:
return None
string_fields = (
"source_game",
"runtime_view",
"source_scene_sha256",
"runtime_scene_sha256",
"scene_action",
"binary_action",
"game_exe_sha256",
"game_dll_sha256",
"created_at",
)
if not all(isinstance(value.get(name), str) for name in string_fields):
return None
links_created = value.get("links_created")
if (
isinstance(links_created, bool)
or not isinstance(links_created, int)
or links_created < 0
):
return None
return LegacyOverlayResult(
source_game=value["source_game"], source_game=value["source_game"],
runtime_view=value["runtime_view"], runtime_view=value["runtime_view"],
source_scene_sha256=value["source_scene_sha256"], source_scene_sha256=value["source_scene_sha256"],

View File

@@ -105,7 +105,7 @@ ACTIVITY_LOG_TRIM_LINES = 500
TRANSFER_RATE_WINDOW_SECONDS = 3.0 TRANSFER_RATE_WINDOW_SECONDS = 3.0
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2 TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
TRANSFER_RATE_IDLE_SECONDS = 2.0 TRANSFER_RATE_IDLE_SECONDS = 2.0
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 650 DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 300
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000 DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16 DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
DASHBOARD_TRANSITION_STEPS = 9 DASHBOARD_TRANSITION_STEPS = 9
@@ -120,6 +120,7 @@ SERVER_STATUS_STALE_SECONDS = 5 * 60
DASHBOARD_STARTUP_COMPONENTS = frozenset( DASHBOARD_STARTUP_COMPONENTS = frozenset(
{ {
"broadcast", "broadcast",
"cached_content",
"compatibility", "compatibility",
"launcher_update", "launcher_update",
"logo", "logo",
@@ -130,6 +131,7 @@ DASHBOARD_STARTUP_COMPONENTS = frozenset(
"version", "version",
} }
) )
DASHBOARD_STARTUP_REQUIRED_COMPONENTS = frozenset({"cached_content"})
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -559,9 +561,9 @@ def dashboard_preloader_delay(
def dashboard_content_ready(ready_components: set[str] | frozenset[str]) -> bool: def dashboard_content_ready(ready_components: set[str] | frozenset[str]) -> bool:
"""Return whether every user-visible startup component has settled.""" """Return whether cached content is ready for an interactive dashboard."""
return DASHBOARD_STARTUP_COMPONENTS.issubset(ready_components) return DASHBOARD_STARTUP_REQUIRED_COMPONENTS.issubset(ready_components)
def dashboard_transition_opacity(step: int, *, fading_in: bool) -> float: def dashboard_transition_opacity(step: int, *, fading_in: bool) -> float:
@@ -1284,6 +1286,7 @@ def run_dashboard(paths: AppPaths) -> None:
startup_timeout_after: str | None = None startup_timeout_after: str | None = None
startup_progress_after: str | None = None startup_progress_after: str | None = None
startup_progress_target = 0.0 startup_progress_target = 0.0
remote_refresh_started = False
root.deiconify() root.deiconify()
root.lift() root.lift()
startup_overlay.lift() startup_overlay.lift()
@@ -1375,6 +1378,7 @@ def run_dashboard(paths: AppPaths) -> None:
set_root_opacity(1.0) set_root_opacity(1.0)
root.lift() root.lift()
root.focus_set() root.focus_set()
root.after_idle(start_remote_content_refresh)
def fade_dashboard_in(step: int = 0) -> None: def fade_dashboard_in(step: int = 0) -> None:
if not set_root_opacity( if not set_root_opacity(
@@ -3389,6 +3393,13 @@ def run_dashboard(paths: AppPaths) -> None:
threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start() threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start()
refresh_launcher_update_check() refresh_launcher_update_check()
def start_remote_content_refresh() -> None:
nonlocal remote_refresh_started
if remote_refresh_started:
return
remote_refresh_started = True
refresh_remote_content()
def release_game_launch_controls( def release_game_launch_controls(
client_id: int, client_id: int,
*, *,
@@ -4282,15 +4293,19 @@ def run_dashboard(paths: AppPaths) -> None:
if cached_launcher_update is not None: if cached_launcher_update is not None:
show_launcher_update_check(cached_launcher_update, log_reason=False) show_launcher_update_check(cached_launcher_update, log_reason=False)
refresh_runtime_compatibility() refresh_runtime_compatibility()
mark_startup_component_ready(
"cached_content",
"Cached dashboard content ready",
)
poll_events() poll_events()
load_official_logo() load_official_logo()
update_startup_progress("Loading online dashboard content…") update_startup_progress("Loading online dashboard content…")
startup_overlay.update_idletasks() startup_overlay.update_idletasks()
startup_timeout_after = root.after( if not startup_transition_started:
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS, startup_timeout_after = root.after(
timeout_startup_preloader, DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
) timeout_startup_preloader,
refresh_remote_content() )
maybe_start_dashboard_transition() maybe_start_dashboard_transition()
try: try:
root.mainloop() root.mainloop()