From c7b1dae471db3bcfdf0810b3a60b2fcaba3056d5 Mon Sep 17 00:00:00 2001 From: Elfaria <319984589+ElfariaKensinton@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:24:21 +0300 Subject: [PATCH 1/2] Reduce game launcher preparation and post-Dock startup delays --- Makefile | 5 +- docs/launch-performance.md | 11 ++ dylib/feats/compat_run.sh | 321 ++++++++++++++++++++++++++++++++- dylib/tests/bridgecheck.sh | 5 +- dylib/tests/launchercheck.sh | 2 +- dylib/tests/launchperfcheck.py | 80 ++++++++ dylib/tests/routecheck.sh | 4 +- dylib/tests/settingscheck.sh | 2 +- 8 files changed, 420 insertions(+), 10 deletions(-) create mode 100644 docs/launch-performance.md create mode 100644 dylib/tests/launchperfcheck.py diff --git a/Makefile b/Makefile index 976d3bd5..b2d5034b 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ DEPS := $(OBJS:.o=.d) tests-list overlay-shim overlay-shim-install overlay-shim-tests \ overlay-shim-bench iconmaker icon \ appinfo helpers-install ntdll-resolve bridge runcheck compatcheck envpasscheck \ - compatsvc-check prefixstate-check defaults-check compatpage-check defaults-drift shortcutcheck scriptcheck envcheck buildcheck settingscheck routecheck bridgecheck gamedrivecheck launchercheck launch-shell activatecheck FORCE + compatsvc-check prefixstate-check defaults-check compatpage-check defaults-drift shortcutcheck scriptcheck envcheck buildcheck settingscheck routecheck bridgecheck gamedrivecheck launchercheck launchperfcheck launch-shell activatecheck FORCE APP_PAYLOAD := app/Sources/NotProtonApp/Resources/payload @@ -144,6 +144,9 @@ launchercheck: @if [ ! -f dylib/tests/launchercheck.sh ]; then $(call SKIP,launchercheck,dylib/tests/launchercheck.sh); exit 0; fi; \ sh dylib/tests/launchercheck.sh dylib/feats/compat_run.sh +launchperfcheck: + python3 dylib/tests/launchperfcheck.py dylib/feats/compat_run.sh + .PHONY: mincheck mincheck: @sh dylib/tests/mincheck.sh "$(APP_BUNDLE)" "$(MIN_VER)" "$(GLASS_SDK)" diff --git a/docs/launch-performance.md b/docs/launch-performance.md new file mode 100644 index 00000000..3148f141 --- /dev/null +++ b/docs/launch-performance.md @@ -0,0 +1,11 @@ +# Launch preparation and Dock startup + +The launcher checks completed Steam common-redist scripts and existing prefix settings before starting Windows helpers. Unsupported scripts, missing completion markers, changed settings, or failed Python checks follow the normal preparation path. The optional checks use the system Python 3 in isolated mode. + +A completed install-script check starts Wine and SamSs in the background while Steam hands off to the game. It uses a command to trigger automatic Wine initialization once; explicitly running `wineboot --init` at this point starts a second service manager and can make SamSs initialization fail. Prefix lock descriptors are closed before starting persistent Wine processes. The generated macOS launcher replaces its shell with Wine using `exec`. + +Correct font links are preserved. Bridge and launcher metadata caches are reused only while their dependencies and launch configuration match. Changed files invalidate the caches. LaunchServices registration is repeated when the installed bundle changes. The normal game-exit teardown still stops the prefix server. + +`TIMING` records in `notproton-run.log` identify preparation stages and macOS bundle invocation. In local Subnautica 2 tests, bundle invocation to the main game process dropped from roughly four seconds to about one to two seconds. One normal relaunch reached the main game process in 9.98 seconds overall. These observations exclude loading the menu or a playable world and are not performance guarantees. + +Run `make launchperfcheck settingscheck launchercheck bridgecheck routecheck gamedrivecheck envcheck buildcheck seedcheck` to exercise the fast paths and existing launcher behavior without launching Steam or Wine. diff --git a/dylib/feats/compat_run.sh b/dylib/feats/compat_run.sh index a1b86b48..b289e173 100755 --- a/dylib/feats/compat_run.sh +++ b/dylib/feats/compat_run.sh @@ -5,6 +5,67 @@ set -e verb="$1" shift || true +# An already-completed common-redist check needs no Wine process. +case "$verb:$1" in + run:*/legacycompat/iscriptevaluator.exe) + np_eval=$(printf '%s' "$2" | tr '\\' '/') + if [ -f "$np_eval" ] && [ -n "$STEAM_COMPAT_DATA_PATH" ] && + /usr/bin/python3 -I - "$np_eval" "$STEAM_COMPAT_DATA_PATH/pfx" <<'NP_COMPLETED_REDIST' +# Conservative offline fast path for Steam's already-completed common-redist script. +import sys,re +from pathlib import Path +try: + script,prefix=map(Path,sys.argv[1:]) + text=script.read_text() + tokens=re.findall(r'"(?:\\.|[^"\\])*"|[{}]|[^\s{}"]+',text) + def decode(t): + assert t.startswith('"') and t.endswith('"') + return re.sub(r'\\([\\"])',r'\1',t[1:-1]) + def obj(i): + result={} + while i> "$STEAM_COMPAT_DATA_PATH/notproton-run.log" + np_redist_complete=1 + fi + ;; +esac + + # hook_launch.c passes the launch options through a shell before this script runs, matching # Linux Steam. A NAME=value option placed ahead of %command% is an environment variable, # and anything after %command% is a launch argument passed to the game. @@ -251,6 +312,8 @@ if [ -n "$STEAM_COMPAT_DATA_PATH" ]; then else log=/dev/null fi +np_timing() { printf 'TIMING %s %s\n' "$(date +%s)" "$*" >> "$log"; } +np_timing launcher-entry [ "$(stat -f %z "$log" 2>/dev/null || echo 0)" -gt 262144 ] \ && : > "$log" || true { @@ -268,6 +331,7 @@ fi } >> "$log" 2>&1 || true stage_step="startup" +np_timing "startup" # shellcheck disable=SC2329 # the trap below invokes this report_early_exit() { status=$? @@ -910,7 +974,106 @@ video_lines() { printf '%s\r\n' '[HKEY_CURRENT_USER\Control Panel\Desktop]' "$dpi_value" '' } +# Check the requested values on disk before starting Wine just to import them. +# Missing Python, unreadable registries, or unsupported syntax use the normal import. +settings_already_applied() { + [ -x /usr/bin/python3 ] || return 1 + /usr/bin/python3 -I - "$WINEPREFIX" "$settings_file" <<'NP_REG_CHECK' +import sys, re +from pathlib import Path +try: + prefix, requested = map(Path, sys.argv[1:]) + def registry(path, wine=False): + sections, section = {}, None + for line in path.read_text(encoding='utf-8').splitlines(): + if line.startswith('['): + end = line.index(']') + section = line[1:end] + if wine: + section = section.replace('\\\\', '\\') + section = section.casefold() + sections[section] = {} + elif section is not None and (line.startswith('"') or line.startswith('@=')): + match = re.fullmatch(r'("(?:[^"\\]|\\.)*"|@)=(.*)', line) + if not match: + raise ValueError('unsupported value') + sections[section][match[1].casefold()] = match[2] + return sections + roots = {'hkey_local_machine': registry(prefix/'system.reg', True), + 'hkey_current_user': registry(prefix/'user.reg', True)} + for section, values in registry(requested).items(): + deleted = section.startswith('-') + root, key = section.lstrip('-').split('\\', 1) + if root == 'hkey_local_machine' and key.startswith('system\\currentcontrolset\\'): + key = key.replace('system\\currentcontrolset\\', 'system\\controlset001\\', 1) + actual = roots[root].get(key) + if deleted: + assert actual is None + else: + for name, value in values.items(): + if value == '-': + assert actual is None or name not in actual + else: + assert actual is not None and actual.get(name) == value +except Exception: + sys.exit(1) +NP_REG_CHECK +} + import_prefix_settings() { + if NP_SETTINGS_VERB="$verb" /usr/bin/python3 -I - <<'NP_SETTINGS_FAST' +import os,re,sys +from pathlib import Path +try: + p=Path(os.environ['WINEPREFIX']); data=Path(os.environ['STEAM_COMPAT_DATA_PATH']) + def registry(file): + out={};section=None + for line in file.read_text().splitlines(): + if line.startswith('['): + section=line[1:line.index(']')].replace('\\\\','\\').casefold();out[section]={} + elif section is not None and (line.startswith('"') or line.startswith('@=')): + m=re.fullmatch(r'("(?:[^"\\]|\\.)*"|@)=(.*)',line) + if m:out[section][m[1].casefold()]=m[2] + return out + system=registry(p/'system.reg');user=registry(p/'user.reg') + def check(reg,key,name,value): + assert reg.get(key.casefold(),{}).get(name.casefold())==value + for key in ('Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug','Software\\Wow6432Node\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug'): + check(system,key,'"Auto"','"0"') + check(user,'Software\\Wine\\WineDbg','"ShowCrashDialog"','dword:00000000') + check(system,'Software\\Classes\\steam','"URL Protocol"','""') + check(system,'Software\\Classes\\steam\\shell\\open\\command','@',r'"\"C:\\Program Files (x86)\\Steam\\steam.exe\" \"%1\""') + env=os.environ + if env.get('NP_SETTINGS_VERB')=='waitforexitandrun': + retina=env.get('NOTPROTON_RETINA')=='1' + actual=user.get('software\\wine\\mac driver',{}).get('"retinamode"') + assert actual==('"y"' if retina else None) + if retina: + dpi=user.get('control panel\\desktop',{}).get('"logpixels"') or system.get('system\\controlset001\\hardware profiles\\current\\software\\fonts',{}).get('"logpixels"') + assert dpi and dpi.startswith('dword:') and int(dpi[6:],16)>=110 + ownedfile=data/'notproton-hidden-controllers';assert not ownedfile.is_symlink() + owned=set(ownedfile.read_text().split()) if ownedfile.exists() else set() + start='system\\controlset001\\services\\winebus\\devices\\' + existing={k[len(start):] for k in system if k.startswith(start) and re.fullmatch('[0-9a-f]{4}/[0-9a-f]{4}',k[len(start):])} + def ids(s):return set(re.findall(r'0x([0-9a-f]{4})/0x([0-9a-f]{4})',s.lower())) + wanted={a+'/'+b for a,b in ids(env.get('SDL_GAMECONTROLLER_IGNORE_DEVICES',''))}-{a+'/'+b for a,b in ids(env.get('SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT',''))} + wanted|={'057e/2006','057e/2007','057e/2009'} + if env.get('NP_SETTINGS_VERB')=='run':wanted|=owned + wanted-=existing-owned + if env.get('NOTPROTON_RAW_CONTROLLERS')=='1':wanted=set() + assert wanted==owned and wanted<=existing + for controller in wanted:check(system,start+controller,'"Hidraw"','dword:00000000') + marker=data/'notproton-disable-hidraw';assert not marker.is_symlink() + disabled=system.get('system\\controlset001\\services\\winebus',{}).get('"disablehidraw"')=='dword:00000001' + if env.get('NOTPROTON_DISABLE_HIDRAW')=='1':assert disabled and marker.exists() + else:assert not marker.exists() or not disabled +except Exception: + sys.exit(1) +NP_SETTINGS_FAST + then + echo "=== prefix settings verified in one pass ===" >> "$log" 2>&1 || true + return 0 + fi if [ "$NOTPROTON_RETINA" = "1" ]; then retina_line='"RetinaMode"="y"' else @@ -959,8 +1122,13 @@ import_prefix_settings() { without_lock_fds "$WINELOADER" wineboot --init >> "$log" 2>&1 || true return 0 fi + if settings_already_applied; then + import_status=0 + echo "=== prefix settings unchanged; skipped Wine registry helper ===" >> "$log" 2>&1 || true + else without_lock_fds "$WINELOADER" reg import "C:\\${settings_file##*/}" >> "$log" 2>&1 \ && import_status=0 || import_status=$? + fi rm -f "$settings_file" if [ "$import_status" -eq 0 ] && [ "$hidraw_planned" -eq 1 ] && [ "$NOTPROTON_DISABLE_HIDRAW" != "1" ]; then rm -f "$hidraw_file" @@ -990,6 +1158,7 @@ import_prefix_settings() { } stage_step="runner check" +np_timing "runner check" if [ -z "$np_build" ] || [ ! -d "$CX_ROOT/lib/wine" ]; then echo "=== build ${np_build:-(none recorded)} behind this compatibility tool is not set up, set it up in NotProton ===" >> "$log" 2>&1 || true show_alert "CrossOver is not set up" "The CrossOver build behind $(alert_safe "$np_display") is not set up. Set it up in NotProton, or pick another compatibility tool for this game." @@ -1380,6 +1549,27 @@ map_game_drive() { # Implementation of Proton's create_fonts_symlinks(). link_fonts() { + if [ -x /usr/bin/python3 ] && /usr/bin/python3 -I - "$WINEPREFIX" "$np_support" "$CX_ROOT" "$app_id" <<'NP_FONTS' +import sys, os +from pathlib import Path +prefix, support, runner = map(Path, sys.argv[1:4]) +fonts = prefix/'drive_c/windows/Fonts' +fonts.mkdir(parents=True, exist_ok=True) +for directory in (support/'fonts', runner/'share/wine/fonts'): + for src in sorted(directory.glob('*.ttf')) + sorted(directory.glob('*.ttc')): + if not src.is_file(): continue + if sys.argv[4] in ('1313860', '1506830') and src.name == 'arial.ttf': + src = directory/'alt'/src.name + dst = fonts/src.name + if dst.is_symlink(): + if os.readlink(dst) == str(src): continue + dst.unlink() + elif dst.exists(): continue + dst.symlink_to(src) +NP_FONTS + then + return 0 + fi [ -f "$WINEPREFIX/system.reg" ] || return 0 fonts_dir="$WINEPREFIX/drive_c/windows/Fonts" mkdir -p "$fonts_dir" 2>> "$log" || return 0 @@ -1415,6 +1605,7 @@ native_vc_runtime() { if [ -n "$STEAM_COMPAT_DATA_PATH" ]; then export WINEPREFIX="$STEAM_COMPAT_DATA_PATH/pfx" stage_step="prefix lock" + np_timing "prefix lock" prepare_prefix_directory msync_from=environment if np_from_defaults WINEMSYNC; then msync_from=${np_defaults_file##*/}; fi @@ -1429,8 +1620,10 @@ if [ -n "$STEAM_COMPAT_DATA_PATH" ]; then printf '%s' "$WINEMSYNC" \ > "$STEAM_COMPAT_DATA_PATH/notproton-msync" 2>/dev/null || true stage_step="prefix build check" + np_timing "prefix build check" note_other_build stage_step="prefix arch check" + np_timing "prefix arch check" refuse_foreign_prefix claim_prefix stage_step="prefix switch check" @@ -1441,23 +1634,43 @@ if [ -n "$STEAM_COMPAT_DATA_PATH" ]; then else echo "avx: ROSETTA_ADVERTISE_AVX=$ROSETTA_ADVERTISE_AVX from $avx_from" >> "$log" 2>&1 || true fi - without_lock_fds "$WINESERVER" -k >> "$log" 2>&1 || true + # Reuse the install-script server when its boot environment is identical. + server_config=$(printf '%s\n' "$CX_ROOT" "$WINEMSYNC" "$ROSETTA_ADVERTISE_AVX" "$FEX_X87REDUCEDPRECISION") + server_record="$STEAM_COMPAT_DATA_PATH/notproton-server-config" + if [ "$(cat "$server_record" 2>/dev/null)" != "$server_config" ]; then + without_lock_fds "$WINESERVER" -k >> "$log" 2>&1 || true + without_lock_fds "$WINESERVER" -w >> "$log" 2>&1 || true + printf '%s\n' "$server_config" > "$server_record" || true + fi + if [ "${np_redist_complete:-0}" = 1 ]; then + # Overlap Wine initialization with Steam's helper-to-game handoff. + without_lock_fds "$WINESERVER" -p60 >> "$log" 2>&1 || true + WINEDEBUG=err+all,fixme-all "$WINELOADER" cmd.exe /d /c "sc start SamSs" >> "$log" 2>&1 8>&- 9>&- < /dev/null & + np_timing background-wine-init + exit 0 + fi stage_step="prefix seed" + np_timing "prefix seed" seed_prefix_from_template stage_step="profile layout" + np_timing "profile layout" if ! lay_out_proton_profile; then show_alert "Saved-game folders need attention" "NotProton left the conflicting files in place. Open this prefix's notproton-run.log for details before trying again." exit 1 fi [ "$verb" != waitforexitandrun ] || echo "video: RetinaMode=${NOTPROTON_RETINA:-0}" >> "$log" 2>&1 || true stage_step="game drive" + np_timing "game drive" map_game_drive stage_step="prefix settings" + np_timing "prefix settings" import_prefix_settings stage_step="fonts" + np_timing "fonts" link_fonts # A prefix that Wine built just now only has dosdevices from this point on. stage_step="game drive" + np_timing "game drive" map_game_drive fi @@ -1548,6 +1761,42 @@ link_legacy_steam_dll() { fi } +np_cache() { + /usr/bin/python3 -I - "$@" <<'NP_METADATA_CACHE' +import sys,os,json,stat,hashlib,shlex +from pathlib import Path +try: + mode,cache,parameters,values,*paths=sys.argv[1:] + snapshot=[] + def record(p): + try: + st=os.lstat(p);s=[p,st.st_mode] if stat.S_ISDIR(st.st_mode) else [p,st.st_mode,st.st_ino,st.st_size,st.st_mtime_ns,st.st_ctime_ns] + if stat.S_ISLNK(st.st_mode): + s.append(os.readlink(p)) + try: + target=os.stat(p);s.extend([target.st_ino,target.st_size,target.st_mtime_ns,target.st_ctime_ns]) + except FileNotFoundError:s.append('broken') + snapshot.append(s) + except FileNotFoundError:snapshot.append([p,'missing']) + for p in paths: + record(p) + if os.path.isdir(p) and not os.path.islink(p): + for parent,dirs,files in os.walk(p,followlinks=False): + for child in sorted(dirs+files):record(os.path.join(parent,child)) + signature=hashlib.sha256(json.dumps([parameters,snapshot],sort_keys=True).encode()).hexdigest() + path=Path(cache) + if mode=='read': + saved=json.loads(path.read_text());assert signature==saved['signature'] + assert set(saved['values'])<=set(('WINEDLLPATH','WINEDLLOVERRIDES','STEAM_COMPAT_CLIENT_INSTALL_PATH','WINELOADER')) + print('; '.join('export '+key+'='+shlex.quote(value) for key,value in saved['values'].items())) + else: + pairs=values.split('\n');vals=dict(x.split('=',1) for x in pairs if '=' in x) + temp=path.with_suffix('.tmp');temp.write_text(json.dumps(dict(signature=signature,values=vals)));temp.replace(path) +except Exception: + sys.exit(1) +NP_METADATA_CACHE +} + bridge_files="steamclient64.dll steamclient.dll tier0_s64.dll vstdlib_s64.dll" bridge_files="$bridge_files lsteamclient.dll lsteamclient.so steam.exe" # A clone cannot cross drives, so a prefix on another drive clones from a copy of the @@ -1573,7 +1822,17 @@ place_bridge_file() { cp -c -fp "$origin" "$2" 2>/dev/null || cp -fp "$bridge_src/$1" "$2" 2>> "$log" } if [ -d "$bridge_src" ] && [ -n "$WINEPREFIX" ]; then + np_bridge_parameters="$WINEDLLPATH|$WINEDLLOVERRIDES|$CX_ROOT|$np_build|$wine_unix|$STEAM_COMPAT_CLIENT_INSTALL_PATH|$STEAM_COMPAT_INSTALL_PATH" + np_bridge_cache="$STEAM_COMPAT_DATA_PATH/.notproton-bridge-cache.json" + np_bridge_stamp() { + np_cache "$1" "$np_bridge_cache" "$np_bridge_parameters" "$2" "$0" "$bridge_src" "$prefix_steam/steamclient64.dll" "$prefix_steam/steamclient.dll" "$prefix_steam/tier0_s64.dll" "$prefix_steam/vstdlib_s64.dll" "$prefix_steam/lsteamclient.dll" "$prefix_steam/lsteamclient.so" "$prefix_steam/steam.exe" "$prefix_steam/Steam.dll" "$CX_ROOT/lib/wine/x86_64-windows/ntdll.dll" "$CX_ROOT/lib/wine/i386-windows/ntdll.dll" "$CX_ROOT/lib/wine/aarch64-windows/ntdll.dll" "$CX_ROOT/lib/wine/${wine_unix##*/}/lsteamclient.so" "$WINEPREFIX"/drive_c/windows/system32/msvc*.dll "$WINEPREFIX"/drive_c/windows/system32/vc*.dll "$WINEPREFIX"/drive_c/windows/system32/concrt*.dll "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/GameOverlayRenderer64.dll" "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/Steam.dll" "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/SteamService.exe" "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/iscriptevaluator.exe" "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/steamclient.dll" "$STEAM_COMPAT_CLIENT_INSTALL_PATH/legacycompat/steamclient64.dll" "$WINEPREFIX/drive_c/windows/system32/lsteamclient.dll" "$WINEPREFIX/drive_c/windows/syswow64/lsteamclient.dll" "$WINEPREFIX/drive_c/windows/system32/Steam.dll" "$(dirname "$STEAM_COMPAT_INSTALL_PATH")/Steam.dll" + } + if np_cached_env=$(np_bridge_stamp read ""); then + eval "$np_cached_env" + np_timing bridge-cache-reused + else stage_step="bridge staging" + np_timing "bridge staging" mkdir -p "$prefix_steam" bridge_cache="" if [ -n "$STEAM_COMPAT_DATA_PATH" ] && ! same_volume "$bridge_src" "$STEAM_COMPAT_DATA_PATH" \ @@ -1644,6 +1903,10 @@ if [ -d "$bridge_src" ] && [ -n "$WINEPREFIX" ]; then echo "WINEDLLPATH=$WINEDLLPATH" >> "$log" 2>&1 || true echo "WINEDLLOVERRIDES=$WINEDLLOVERRIDES" >> "$log" 2>&1 || true echo "STEAM_COMPAT_CLIENT_INSTALL_PATH=$STEAM_COMPAT_CLIENT_INSTALL_PATH" >> "$log" 2>&1 || true + np_bridge_stamp write "WINEDLLPATH=$WINEDLLPATH +WINEDLLOVERRIDES=$WINEDLLOVERRIDES +STEAM_COMPAT_CLIENT_INSTALL_PATH=$STEAM_COMPAT_CLIENT_INSTALL_PATH" || true + fi fi export WINEDEBUG="${WINEDEBUG:-err+all,fixme-all}" @@ -1662,6 +1925,8 @@ esac shim_exe="C:\\Program Files (x86)\\Steam\\steam.exe" if [ "$foreground" = 0 ]; then + "$WINESERVER" -p60 >> "$log" 2>&1 || true + np_timing helper-start status=0 # set -e would exit before the status is read case "$verb" in # runinprefix is the one verb the Linux client keeps on the raw loader. @@ -1675,6 +1940,10 @@ if [ "$foreground" = 0 ]; then "$WINELOADER" "$shim_exe" "$@" >> "$log" 2>&1 || status=$? ;; esac + if [ "$status" -eq 0 ]; then + "$WINESERVER" -p60 >> "$log" 2>&1 || true + fi + np_timing helper-finished echo "=== helper exited status=$status ===" >> "$log" 2>&1 || true exit $status fi @@ -1699,6 +1968,15 @@ game_name_xml=$(printf '%s' "$game_name" | sed -e 's/&/\&/g' -e 's//dev/null || true loader_work=$(mktemp -d "$loader_root/.build.XXXXXX") loader_stage="$loader_work/new/$bundle_name.app" @@ -1777,12 +2055,36 @@ if [ -n "\$NOTPROTON_FEATURE_DYLIBS" ]; then export DYLD_INSERT_LIBRARIES="\${DYLD_INSERT_LIBRARIES:+\$DYLD_INSERT_LIBRARIES:}\$NOTPROTON_FEATURE_DYLIBS" fi [ -n "\$NOTPROTON_GAME_CWD" ] && cd "\$NOTPROTON_GAME_CWD" -"$WINELOADER" "\$@" -exit \$? +exec "$WINELOADER" "\$@" LAUNCHER chmod +x "$loader_macos/launcher" bundle_listing() { + # Compare in one process instead of spawning stat/cksum/readlink per entry. + if [ -x /usr/bin/python3 ] && /usr/bin/python3 -I - "$1" <<'NP_BUNDLE_CHECK' +import os, sys, stat, hashlib, json +root = sys.argv[1] +entries = [] +for directory, dirs, files in os.walk(root, followlinks=False): + for name in dirs + files: + path = os.path.join(directory, name) + rel = os.path.relpath(path, root) + info = os.lstat(path) + if stat.S_ISLNK(info.st_mode): + entries.append((rel, 'l', os.readlink(path))) + elif stat.S_ISDIR(info.st_mode): + entries.append((rel, 'd')) + elif stat.S_ISREG(info.st_mode): + with open(path, 'rb') as stream: + digest = hashlib.sha256(stream.read()).hexdigest() + entries.append((rel, 'f', stat.S_IMODE(info.st_mode), digest)) + else: + raise ValueError('unsupported bundle entry') +print(json.dumps(sorted(entries), ensure_ascii=True)) +NP_BUNDLE_CHECK + then + return 0 + fi (cd "$1" && find . | LC_ALL=C sort | while IFS= read -r p; do if [ -L "$p" ]; then printf 'l %s %s\n' "$p" "$(readlink "$p")" @@ -1843,7 +2145,11 @@ install_bundle() ( fi rm -rf "$loader_work" || true ) +np_timing bundle-install install_bundle +np_timing bundle-ready +np_bundle_stamp write "WINELOADER=$WINELOADER" || true +fi kill_wine_prefix() { "$WINESERVER" -k >> "$log" 2>&1 || true @@ -2141,9 +2447,16 @@ set -- \ --env NOTPROTON_GAME_CWD="$game_cwd" "$@" lsregister="/System/Library/Frameworks/CoreServices.framework/Versions/A" lsregister="$lsregister/Frameworks/LaunchServices.framework/Support/lsregister" -[ -x "$lsregister" ] && "$lsregister" -f "$loader_app" >> "$log" 2>&1 || true +# Register only a new or changed bundle, keeping repeated launches off this path. +registration_stamp="$loader_root/.launchservices-registered" +if [ ! -f "$registration_stamp" ] || [ "$loader_app/Contents/Info.plist" -nt "$registration_stamp" ]; then + if [ -x "$lsregister" ] && "$lsregister" -f "$loader_app" >> "$log" 2>&1; then + touch "$registration_stamp" || true + fi +fi np_started=$(date +%s) np_terminated=0 +np_timing launching-bundle open -n -W -a "$loader_app" "$@" >> "$log" 2>&1 & open_pid=$! /usr/bin/caffeinate -d -i -w "$open_pid" >> "$log" 2>&1 & diff --git a/dylib/tests/bridgecheck.sh b/dylib/tests/bridgecheck.sh index 47a2c846..34b2ecee 100644 --- a/dylib/tests/bridgecheck.sh +++ b/dylib/tests/bridgecheck.sh @@ -13,10 +13,13 @@ esac block=$(printf '%s\n' "$block" | sed '$d') legacy=$(sed -n '/^install_legacycompat() {/,/^}/p' "$SRC") [ -n "$legacy" ] || { echo "FAIL: install_legacycompat not found in $SRC"; exit 1; } -body='same_volume() { [ -z "$cross" ]; } +body='np_cache() { return 1; } +np_timing() { :; } +same_volume() { [ -z "$cross" ]; } volume_clones() { :; } show_alert() { printf "alert: %s\n" "$1" >> "$log"; } '"$block +fi fi" work=$(mktemp -d) diff --git a/dylib/tests/launchercheck.sh b/dylib/tests/launchercheck.sh index a7cd3f7c..8ff92e9f 100644 --- a/dylib/tests/launchercheck.sh +++ b/dylib/tests/launchercheck.sh @@ -4,7 +4,7 @@ set -e SRC="${1:-$(dirname "$0")/../feats/compat_run.sh}" [ -f "$SRC" ] || { echo "launchercheck: $SRC not present, skipped"; exit 0; } -funcs=$(sed -n '/^bundle_listing() {$/,/^install_bundle$/p' "$SRC" | sed '$d') +funcs=$(sed -n '/^bundle_listing() {$/,/^install_bundle$/p' "$SRC" | sed '$d; /^np_timing /d') name_lines=$(sed -n '/^bundle_name=\$(printf/,/^\[ -n "\$bundle_name" \] || bundle_name=/p' "$SRC") launcher_body=$(sed -n '/^cat > "\$loader_macos\/launcher" < 1 else Path(__file__).parents[1] / 'feats/compat_run.sh').read_text() + + +def embedded(marker): + return re.search(r"<<'" + marker + r"'\n(.*?)\n" + marker, SOURCE, re.S)[1] + + +class LaunchFastPaths(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def run_code(self, marker, *args): + return subprocess.run([sys.executable, '-I', '-c', embedded(marker), *map(str, args)], capture_output=True, text=True) + + def install_fixture(self, completed=True, action='run process'): + script = self.root / 'script.vdf' + script.write_text('"evaluatorscript" { "0" { "appid" "228980" "mode" "1" ' + '"compat_installscript" { "' + action + '" { "dxsetup" { ' + '"hasrunkey" "HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\DirectX\\\\Jun2010" ' + '"process 1" "%INSTALLDIR%\\\\_CommonRedist\\\\DirectX\\\\Jun2010\\\\DXSETUP.exe" ' + '"command 1" "/silent" "nocleanup" "1" } } } } }') + (self.root / 'system.reg').write_text('[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\DirectX\\\\Jun2010]\n' + '"dxsetup"=dword:0000000' + ('1' if completed else '0') + '\n') + return script + + def test_completed_install_check(self): + self.assertEqual(self.run_code('NP_COMPLETED_REDIST', self.install_fixture(), self.root).returncode, 0) + + def test_incomplete_install_requires_evaluator(self): + self.assertNotEqual(self.run_code('NP_COMPLETED_REDIST', self.install_fixture(False), self.root).returncode, 0) + + def test_unknown_action_requires_evaluator(self): + self.assertNotEqual(self.run_code('NP_COMPLETED_REDIST', self.install_fixture(action='registry'), self.root).returncode, 0) + + def test_malformed_script_requires_evaluator(self): + script = self.install_fixture() + script.write_text('broken') + self.assertNotEqual(self.run_code('NP_COMPLETED_REDIST', script, self.root).returncode, 0) + + def test_cache_dependency_changes(self): + cache = self.root / 'cache.json' + dependency = self.root / 'bridge.dll' + dependency.write_text('original') + args = (cache, 'environment', 'WINELOADER=/runner/wine', dependency) + self.assertEqual(self.run_code('NP_METADATA_CACHE', 'write', *args).returncode, 0) + self.assertEqual(self.run_code('NP_METADATA_CACHE', 'read', *args).returncode, 0) + dependency.write_text('changed') + self.assertNotEqual(self.run_code('NP_METADATA_CACHE', 'read', *args).returncode, 0) + dependency.unlink() + self.assertNotEqual(self.run_code('NP_METADATA_CACHE', 'read', *args).returncode, 0) + + def test_bundle_detects_symlink_change(self): + bundle = self.root / 'Game.app' + bundle.mkdir() + link = bundle / 'library.dylib' + link.symlink_to('/one') + before = self.run_code('NP_BUNDLE_CHECK', bundle) + link.unlink() + link.symlink_to('/two') + after = self.run_code('NP_BUNDLE_CHECK', bundle) + self.assertEqual(before.returncode, 0) + self.assertEqual(after.returncode, 0) + self.assertNotEqual(before.stdout, after.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/dylib/tests/routecheck.sh b/dylib/tests/routecheck.sh index 5a9713c3..05fae3f5 100755 --- a/dylib/tests/routecheck.sh +++ b/dylib/tests/routecheck.sh @@ -36,10 +36,10 @@ route() { case_dir=$(mktemp -d "$work/case.XXXXXX") : > "$case_dir/calls" : > "$case_dir/log" - env WINELOADER="$work/wine" STEAM_COMPAT_INSTALL_PATH=/g \ + env WINELOADER="$work/wine" WINESERVER=/usr/bin/true STEAM_COMPAT_INSTALL_PATH=/g \ FAKE_STATUS="${FAKE_STATUS:-0}" FAKE_CALLS="$case_dir/calls" \ log="$case_dir/log" body="$body" verb="$verb" \ - sh -ec 'eval "$body"; echo "fell through foreground=$foreground"' \ + sh -ec 'np_timing() { :; }; eval "$body"; echo "fell through foreground=$foreground"' \ "$verb" "$@" > "$case_dir/out" 2>&1 || true } calls() { cat "$case_dir/calls"; } diff --git a/dylib/tests/settingscheck.sh b/dylib/tests/settingscheck.sh index b567a030..5487ebe5 100644 --- a/dylib/tests/settingscheck.sh +++ b/dylib/tests/settingscheck.sh @@ -10,7 +10,7 @@ wrapper=$(sed -n '/^without_lock_fds() {$/,/^}$/p' "$SRC") [ -n "$wrapper" ] || { echo "FAIL: without_lock_fds not found in $SRC"; exit 1; } helpers="" for helper in controller_ids ids_without hidraw_lines write_owned_controllers plan_hidden_controllers \ - plan_disable_hidraw winebus_lines plan_logpixels video_lines; do + plan_disable_hidraw winebus_lines plan_logpixels video_lines settings_already_applied; do part=$(sed -n "/^$helper() {\$/,/^}\$/p" "$SRC") [ -n "$part" ] || { echo "FAIL: $helper not found in $SRC"; exit 1; } helpers="$helpers From ba39c909d61a29c6b2f21d9cdba51ae26fa79217 Mon Sep 17 00:00:00 2001 From: ElfariaKensington <319984589+ElfariaKensington@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:04:35 +0300 Subject: [PATCH 2/2] Skip completed VC2013 prerequisite checks at game launch --- dylib/feats/compat_run.sh | 2 +- dylib/tests/launchperfcheck.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/dylib/feats/compat_run.sh b/dylib/feats/compat_run.sh index b289e173..938e1576 100755 --- a/dylib/feats/compat_run.sh +++ b/dylib/feats/compat_run.sh @@ -50,7 +50,7 @@ try: key=process['hasrunkey'].casefold() assert key.startswith('hkey_local_machine\\software\\valve\\steam\\apps\\commonredist\\') suffix=key.split('commonredist\\',1)[1] - assert suffix in ('vcredist\\2019','vcredist\\2022','directx\\jun2010') + assert suffix in ('vcredist\\2013','vcredist\\2019','vcredist\\2022','directx\\jun2010') assert process['process 1'].casefold().startswith('%installdir%\\_commonredist\\'+suffix+'\\') # These common-redist completion markers use the redirected view. key=key.replace('hkey_local_machine\\software\\','software\\wow6432node\\',1) diff --git a/dylib/tests/launchperfcheck.py b/dylib/tests/launchperfcheck.py index a90e9332..d1825a3b 100644 --- a/dylib/tests/launchperfcheck.py +++ b/dylib/tests/launchperfcheck.py @@ -39,6 +39,29 @@ def install_fixture(self, completed=True, action='run process'): def test_completed_install_check(self): self.assertEqual(self.run_code('NP_COMPLETED_REDIST', self.install_fixture(), self.root).returncode, 0) + def vcredist_2013_fixture(self, x64_complete=True): + script = self.install_fixture() + text = script.read_text().replace('DirectX\\\\Jun2010', 'vcredist\\\\2013') + text = text.replace('dxsetup', 'x86 12.0.30501').replace('DXSETUP.exe', 'vcredist_x86.exe') + # Two architectures must both be complete before the helper is skipped. + text = text.replace('"nocleanup" "1"', '"nocleanup" "1" } "x64 12.0.30501" { ' + '"hasrunkey" "HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2013" ' + '"process 1" "%INSTALLDIR%\\\\_CommonRedist\\\\vcredist\\\\2013\\\\vcredist_x64.exe" ' + '"nocleanup" "1" "requirement_os" { "Is64BitWindows" "1" }') + script.write_text(text) + (self.root / 'system.reg').write_text('[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2013]\n' + '"x86 12.0.30501"=dword:00000001\n' + '"x64 12.0.30501"=dword:0000000' + ('1' if x64_complete else '0') + '\n') + return script + + def test_completed_vcredist_2013_both_architectures(self): + script = self.vcredist_2013_fixture() + self.assertEqual(self.run_code('NP_COMPLETED_REDIST', script, self.root).returncode, 0) + + def test_incomplete_vcredist_2013_requires_evaluator(self): + script = self.vcredist_2013_fixture(x64_complete=False) + self.assertNotEqual(self.run_code('NP_COMPLETED_REDIST', script, self.root).returncode, 0) + def test_incomplete_install_requires_evaluator(self): self.assertNotEqual(self.run_code('NP_COMPLETED_REDIST', self.install_fixture(False), self.root).returncode, 0)