Skip to content

Optimize launch speed: fast prefix reuse, bridge staging check, and launcher icon caching - #8

Merged
NotProtonNot merged 0 commit into
NotProtonNot:dev-1.1.0from
Ghostkwebb:feat/crossover-preview-dual-arch-and-speedup
Oct 7, 2026
Merged

NotProtonNot merged 0 commit into
NotProtonNot:dev-1.1.0from
Ghostkwebb:feat/crossover-preview-dual-arch-and-speedup

Conversation

@Ghostkwebb

@Ghostkwebb Ghostkwebb commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR focuses specifically on launch speed optimizations and test fixture fixes:

  • Prefix settings fast-path: Avoids running redundant wineboot --init and static reg add calls on existing prefixes where initial setup has already succeeded, while still dynamically applying runtime toggles like Retina mode.
  • Bridge staging check: Skips copying byte-identical Steam bridge files (steamclient64.dll, etc.) on every launch using cmp -s.
  • Launcher icon caching & bundle reuse: Caches client icons with 404 tracking, updates game.icns on art changes, tracks bundle template versions, and skips redundant bundle recreation and LaunchServices re-registration when unchanged.
  • Test fix: Fixes dylib/tests/anchorcheck.c so make sigdb-fixtures passes.

@LaganYT LaganYT left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by reading the diff and surrounding code; not built or tested. Six issues inline. #1 (icon 404 marker) and #5 (tests deleting a real folder) look like quick fixes worth doing before merge.

Comment thread dylib/feats/compat_run.sh Outdated
if [ ! -s "$ico" ] && [ ! -f "$absent" ]; then
url="https://shared.fastly.steamstatic.com/community_assets/images/apps/$app_id/$meta_clienticon.ico"
code=$(curl -fsL --connect-timeout 5 --max-time 20 -w '%{http_code}' -o "$ico.new" "$url" 2>>"$log")
code=$(curl -fsL --connect-timeout 2 --max-time 4 -w '%{http_code}' -o "$ico.new" "$url" 2>>"$log" || echo "none")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing-icon marker is never saved. With || echo "none" appended, a 404 gets recorded as "404none", so the check for "404" never matches. Games with no published icon retry the download on every launch, which can add up to ~4s — the kind of launch delay this PR is trying to remove.

Comment thread dylib/feats/compat_run.sh Outdated

CX_ROOT="$HOME/Library/Application Support/notproton/runners/current"
CX_RUNNERS="$HOME/Library/Application Support/notproton/runners"
CX_ROOT=""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can launch the wrong runner. The app never creates current-rosetta or current-fex, so this always falls through to whichever crossover-* folder find | head -1 returns first. That can be an old, unpatched runner rather than the current one the app set up.

Comment thread dylib/feats/compat_run.sh Outdated
bundle_needs_build=0
if [ ! -d "$loader_app" ] || [ ! -x "$loader_macos/launcher" ] || [ ! -f "$loader_contents/Info.plist" ] || [ ! -e "$loader_macos/wine" ]; then
bundle_needs_build=1
elif ! cmp -s "$WINELOADER" "$loader_macos/wine" 2>/dev/null; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Launcher app settings go stale. Once the per-game launcher app exists, its Info.plist is only rewritten when the wine loader binary changes. If the first launch had no icon, the icon never appears later, and toggling NOTPROTON_HIDE_LAUNCHER_TILE has no effect.

Comment thread dylib/feats/compat_run.sh
base=${f##*/}
case "$base" in
wine|wine.app) continue ;;
esac

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Links can keep pointing into an old runner. When the launcher app is reused, an existing link is left alone if a file with that name already exists. If the runner changes but the loader is byte-identical, the game ends up mixing the old runner's unix libraries with the new runner's DLLs.

updateBlocks: [URL] = UpdateBlock.paths,
legacyCompat: URL = SupportPaths.Steam.legacyCompat,
compatTool: URL = SupportPaths.Steam.compatTool,
compatToolFex: URL = SupportPaths.Steam.compatToolFex,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests delete a real folder. compatToolFex defaults to the real ~/Library/Application Support/Steam/compatibilitytools.d/notproton-fex, and the existing UninstallTests don't override it, so running the suite removes that folder on the developer's machine. Suggest passing a temp path in the tests (or removing the default).

Comment thread dylib/feats/compat_run.sh
if [ ! -f "$setup_marker" ]; then
echo "=== first-time prefix initialization ===" >> "$log" 2>&1 || true
"$WINESERVER" -k >> "$log" 2>&1 || true
stage_step="profile layout"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saves split between FEX and Rosetta. A new pfx-fex / pfx-rosetta prefix gets a one-time copy of saves from pfx, and nothing copies them back. After switching back, recent progress looks lost, and the app's backup/rebuild options only ever touch pfx. Probably worth deciding whether prefixes should share saves or stay intentionally separate.

@LaganYT

LaganYT commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up after building and testing locally (arm64 macOS, compared against main on the same machine):

make all builds cleanly. compatcheck, compatsvc-check, sigcheck, anchorcheck, peicon-fixtures, webpatch-fixtures, spawn-env, panel-behavior, ntdll-resolve and overlay-shim-tests all pass. Two things fail on this branch but not on main:

1. make sigdb-fixtures: 5 of 28 fixtures fail (all 28 pass on main).
pair_impossible, tail_on_all, vtable_va_cstring, vtable_va_max and vtable_va_wild now exit 0, but EXPECTED still says 1. In report_db in dylib/tests/anchorcheck.c, a failed anchor now only counts as an error when is_live is set (if (is_live) bad++). These fixtures fail their anchors on purpose, and pick_live never selects them, so the failure isn't counted. As a result the fixtures depend on which Steam build is installed, and a signature database whose anchors break is no longer caught unless it matches the running client. Either restore the unconditional bad++ for anchor failures or update EXPECTED and the harness to match the new intent.

2. Swift test "Staging reproduces the bridge the python patcher produced" fails (not in main's failures).
This one depends on the machine's real ~/Library/Application Support/NotProton/bridge. That bridge has i386-windows and aarch64-windows ntdll.dll files matching the 27.0.0.40921 pins, but no x86_64-windows, so compared.count (2) != patchedNtdll.count (3). The bridge is probably left over from an earlier partial install, so this may not be a code bug. Still, can switching between the FEX and Rosetta runners leave the shared bridge in that state? If so, 64-bit games would load an unpatched ntdll.

The other 15 Swift failures also fail on main; they need the full app payload (make app-payload), which I didn't build.

@Ghostkwebb

Ghostkwebb commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks a lot for the thorough review and great catches, @LaganYT! Really appreciate you taking the time to read through everything so carefully.

I've addressed all 6 points in the latest commit:

  1. Missing-icon 404 cache: Dropped -f from curl and added pattern matching on the HTTP code. The .absent marker is now properly written on 404s, preventing redundant 4s retries on subsequent launches.
  2. Runner resolution: $CX_RUNNERS/current is now prioritized and checked first for the requested flavor's wine directory (aarch64-unix for FEX, x86_64-unix for Rosetta) before falling back to searching candidate folders.
  3. Launcher app staleness: The bundle builder now checks whether the staged Info.plist matches current icon availability and NOTPROTON_HIDE_LAUNCHER_TILE, triggering a rebuild and lsregister refresh whenever settings change.
  4. Stale helper symlinks: Removed the [ -e ... ] || guard in the bundle reuse path so ln -sfn unconditionally updates helper symlinks to point to the active runner.
  5. Test fixture safety: Added compatToolFex to UninstallTests.swift's test layout using a temporary directory, ensuring test runs never touch the real compatibility tools folder on disk.
  6. Save sharing across architectures: User directories (Saved Games, Documents, AppData/Local, AppData/LocalLow, AppData/Roaming) in secondary prefixes are now symlinked into the base prefix (pfx). Any existing saves from previous runs are merged first, so game progress is preserved and shared seamlessly regardless of which architecture is launched.

@LaganYT LaganYT left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after 4616f0d, then built and tested locally again.

Fixed: the icon 404 marker; the tests no longer touch the real notproton-fex folder (UninstallTests passes); stale launcher Info.plist / tile settings; stale helper links.

Still open: the Rosetta/FEX runner selection (inline). The two earlier test failures are unchanged: make sigdb-fixtures still fails 5 of 28 (the if (is_live) bad++ change in anchorcheck.c), and the Swift ntdll staging test still fails for the same reason as before. The other Swift failures also happen on main; they need make app-payload.

New: the save-sharing change can permanently delete save progress (inline, reproduced). I'd treat that one as blocking.

Comment thread dylib/feats/compat_run.sh Outdated
if [ -d "$curr_u/$fold" ] && [ ! -L "$curr_u/$fold" ]; then
echo "=== merging $fold into base prefix for shared saves ===" >> "$log" 2>&1 || true
merge_user_dir "$curr_u/$fold" "$main_u/$fold" || true
rm -rf "$curr_u/$fold" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can delete save progress. merge_user_dir never overwrites. It skips any file that already exists in the destination, and any subfolder that already exists there (the if [ -n "$rest" ] && [ -e "$dst_dir" ]; then continue check). Its return value is also ignored (|| true), and then this line deletes the source unconditionally.

Reproduced with the functions from this file: base pfx has Saved Games/MyGame/slot1.sav (older), pfx-fex has Saved Games/MyGame/slot1.sav (newer) plus slot2.sav. After share_user_saves, pfx-fex/.../Saved Games is a symlink to the base and the base still holds only the old slot1.sav. The newer slot1.sav and slot2.sav are both gone.

This hits anyone who played under a flavor prefix with the previous commit, and anyone whose game or wineboot later replaces the symlink with a real folder. Suggest only deleting when the merge fully succeeded and nothing was skipped. Otherwise, move the folder aside (e.g. "$fold.notproton-backup") instead of rm -rf, and log it.

Comment thread dylib/feats/compat_run.sh Outdated
fi
fi
else
if [ -d "$CX_RUNNERS/current/lib/wine/x86_64-unix" ] || [ -d "$CX_RUNNERS/current/lib/wine" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Rosetta tool still runs the FEX runner when current points to FEX. The FEX CrossOver build also ships lib/wine/x86_64-unix, and the || [ -d .../current/lib/wine ] fallback matches any runner. So both conditions are true for a FEX current. Running this block against a real install where current -> crossover-27.0.0.40921-fex/CrossOver resolves flavor=rosetta to the FEX runner.

The Rosetta check needs to reject a FEX current, e.g. [ -d current/lib/wine/x86_64-unix ] && [ ! -d current/lib/wine/aarch64-unix ], or check the resolved path for a -fex suffix.

Comment thread dylib/feats/compat_run.sh Outdated
elif [ -d "$CX_RUNNERS/current-rosetta/lib/wine" ]; then
CX_ROOT="$CX_RUNNERS/current-rosetta"
else
rosetta_cand=$(find "$CX_RUNNERS" -maxdepth 3 -type d -name "x86_64-unix" 2>/dev/null | head -1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new find -maxdepth 3 lookups never match (same for the aarch64-unix one above). The runners are laid out as runners/crossover-<ver>[-fex]/CrossOver/lib/wine/x86_64-unix, which is depth 5, so the search always comes back empty and falls through to the older crossover-* lookup. It's dead code as written. If it were deepened, it would have the same problem as above: the FEX runner has x86_64-unix too.

Comment thread dylib/feats/compat_run.sh Outdated
esac
ln -sfn "$f" "$loader_macos/$base" 2>/dev/null || true
done
if [ -s "$icon_cache" ] && [ ! -f "$loader_res/game.icns" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the icon no longer updates when the game's art changes. resolve_icon rebuilds game.icns in $loader_root, but since the plist still has CFBundleIconFile, the bundle isn't rebuilt. This line only copies the file when Resources/game.icns is missing. Before this commit, resolve_icon copied it every time. Copying whenever the source changed (or cmp -s differs) would restore that.

@LaganYT

LaganYT commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

The two test failures from my earlier comment (#8 (comment)) still fail on 4616f0d. I re-ran them locally:

  • make sigdb-fixtures: the same 5 of 28 fixtures still fail (pair_impossible, tail_on_all, vtable_va_cstring, vtable_va_max, vtable_va_wild: expected 1, got 0). This is still the if (is_live) bad++ change in anchorcheck.c.
  • Swift test "Staging reproduces the bridge the python patcher produced" still fails at NtdllPatcherTests.swift:376 (compared.count == build.patchedNtdll.count).

Could you take a look at both, or explain if they're expected?

@LaganYT

LaganYT commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Thanks @Ghostkwebb! I checked 0754eb8: make sigdb-fixtures now passes all 28, and make anchorcheck still passes against a live client, so that one is fixed.

Your comment above describes 4616f0d, and my re-review of that commit (#8 (review)) found that two of those fixes still have problems:

  • #6 Save sharing (blocking): the merge can permanently delete saves. merge_user_dir skips files and subfolders that already exist in pfx, and then rm -rf deletes the flavor prefix's copy anyway. I reproduced a newer save and a FEX-only save being lost. Details are inline at compat_run.sh:418.
  • Support switching between CrossOver Preview builds #2 Runner resolution: the FEX CrossOver build also ships lib/wine/x86_64-unix, so when current points to FEX, the Rosetta tool still resolves to the FEX runner. The new find -maxdepth 3 fallbacks also never match, because the runners are 5 levels deep. Details are inline at lines 65 and 70.

Still waiting on the Swift ntdll staging test (NtdllPatcherTests.swift:376). Mostly I'd like to know whether switching between the FEX and Rosetta runners can leave the shared bridge without x86_64-windows/ntdll.dll, as I described in #8 (comment).

@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Thanks @LaganYT!

I’ve pushed 9f7db36 to address the remaining points:

  • Safe save sharing: Switched the merge to Python to safely copy over newer or missing saves based on timestamps. Replaced rm -rf with a timestamped .notproton-backup folder, so no saves are ever lost.
  • Runner selection: Added a check to ensure Rosetta won't pick a FEX runner when current points to FEX, and cleaned up the folder lookup.
  • Launcher icon updates: The bundle now updates game.icns whenever new art differs from what's already there.
  • Swift ntdll test (NtdllPatcherTests.swift:376): That missing x86_64 file in your bridge was left over from main (which pruned 64-bit when FEX was active). Since this PR adds x86_64 to FEX as well, neither runner prunes it anymore when switching. Plus, at runtime games load patched ntdll directly from the runner's own folder. Once re-staged, the test passes cleanly!

Could you test it now on your side when you get a chance and let me know if everything looks good?

@LaganYT LaganYT left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 9f7db36, then built and tested it locally.

Verified fixed:

  • Save sharing: rerunning my earlier scenario, the newer slot1.sav and the FEX-only slot2.sav both end up in the base prefix, and the original is kept as *.notproton-backup-<ts>. Nothing is lost.
  • Runner selection: against a mock runners folder with both builds, Rosetta → crossover-27.0.0.40921, and FEX → crossover-27.0.0.40921-fex, whether current points to FEX or Rosetta.
  • Launcher icon: now refreshed when game.icns differs.
  • Tests: make all is clean, all make checks pass (including sigdb-fixtures, 28/28), and sh -n passes. The Swift failures are the same payload-dependent ones as on main, plus the ntdll staging test. Your explanation (a leftover bridge from main's FEX pruning, fixed by re-staging) makes sense. I haven't re-staged my real install to confirm it.

Remaining (inline): the new python3 dependency, which I think should be fixed before merge, and a merge that gets stuck on a broken symlink. Minor: the *.notproton-backup-* folders are never cleaned up, and for AppData/Local that can be a lot of disk space.

Comment thread dylib/feats/compat_run.sh Outdated
mkdir -p "$dst" 2>/dev/null || true
if [ -d "$src" ] && [ ! -L "$src" ]; then
echo "=== safely merging $fold into base prefix for shared saves ===" >> "$log" 2>&1 || true
if python3 - "$src" "$dst" 2>>"$log" <<'PY'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New runtime dependency on python3. main's run script never calls Python. On a Mac without the Xcode Command Line Tools (most gamers), /usr/bin/python3 is only a stub: it opens the "install command line developer tools" dialog and exits non-zero. Then the merge fails, the folder is never shared, and because this runs on every launch, the dialog would likely come back each time a game starts.

I'd keep this in POSIX sh (find + cp -p with a [ "$s" -nt "$d" ] check), or check xcode-select -p >/dev/null 2>&1 first and skip the merge (leaving the folder unshared) when the tools aren't installed.

Comment thread dylib/feats/compat_run.sh Outdated
s = os.path.join(root, f)
d = os.path.join(target_dir, f)
try:
if not os.path.exists(d) or os.path.getmtime(s) >= os.path.getmtime(d):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One bad entry blocks sharing for that folder and re-copies it on every launch. A broken symlink anywhere in the tree makes shutil.copy2 raise, so the merge returns 1 and the folder is skipped. It retries, and fails, on every launch, and never gets shared. I reproduced this with one dangling link under AppData/Local.

Because the check is >= and copy2 keeps mtimes, each retry copies every file again, even ones already identical in the base. AppData/Local can hold large shader or engine caches, so that can add launch time.

Suggestions: copy symlinks as links (os.path.islink(s) → os.symlink(os.readlink(s), d), or copy2(..., follow_symlinks=False)) so broken ones don't fail the merge, and use > instead of >=.

@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Oooooh ic ic, completely slipped my mind that macOS throws that CLI tools dialog on clean installs like an idiot XD!

I've pushed dd73919:

  • Replaced Python with a pure POSIX shell traversal (find + [ "$s" -nt "$d" ]), so zero extra dependencies or dialog popups.
  • Symlinks (even dangling ones) are mirrored as links without choking the merge.
  • Strictly newer (-nt) or missing files are copied, skipping identical files.
  • Kept just a single .notproton-backup folder per directory instead of piling them up to save disk space.

Should be all clean now, let me know what you think!

@LaganYT LaganYT left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed dd73919, then built and tested it locally.

Verified:

  • python3 is gone; the merge is plain sh.
  • make all is clean, all make checks pass (sigdb-fixtures 28/28), and sh -n passes. The Swift results are the same as on 9f7db36.
  • My earlier save scenarios now work: the newer save and the FEX-only save both reach pfx, and filenames with leading and trailing spaces survive. A dangling link under AppData/Local no longer blocks the merge, and the folder is now shared.

Remaining (inline): a symlink in the flavor prefix can still overwrite a real save in pfx (reproduced; one-line fix). Minor: the single backup is deleted on a later re-merge.

Comment thread dylib/feats/compat_run.sh Outdated
target=$(readlink "$rel" 2>/dev/null || true)
if [ -n "$target" ]; then
mkdir -p "$(dirname "$_d_root/$rel")" 2>/dev/null || exit 1
ln -sfn "$target" "$_d_root/$rel" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A symlink in the flavor prefix overwrites a real save in the base. ln -sfn runs unconditionally, so when pfx already has a real file at the same path, it gets replaced by the link. Regular files use [ ! -e "$d" ] || [ "$s" -nt "$d" ], but symlinks skip that check.

Reproduced: base Documents/G/profile.dat is a real file, and pfx-fex has Documents/G/profile.dat -> /nonexistent/profile.dat. After the merge, the base file is a dangling link, and the .notproton-backup only holds the link, so the save is gone from both prefixes.

Suggest only creating the link when nothing is there: if [ ! -e "$_d_root/$rel" ] && [ ! -L "$_d_root/$rel" ]; then ln -s ...; fi.

Comment thread dylib/feats/compat_run.sh Outdated
echo "=== safely merging $fold into base prefix for shared saves ===" >> "$log" 2>&1 || true
if merge_user_tree "$src" "$dst" 2>>"$log"; then
backup="${src}.notproton-backup"
[ -e "$backup" ] && rm -rf "$backup" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this deletes the previous backup the next time the folder is merged, e.g. if a game or wineboot replaces the symlink with a real folder again. That previous backup is the only copy of any file the first merge skipped because the base's copy was newer. I confirmed a second merge of Saved Games replaces the first backup's contents entirely.

Suggest keeping the old backup: merge into it, or fall back to a timestamped name only when .notproton-backup already exists. That still avoids piling up a new copy on every launch.

@NotProtonNot

NotProtonNot commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

There is good stuff in here. A good chunk of this will come in (though I will make some changes, rewrite stuff). The launch speed improvements - a good chunk of that is great, The handling of FEX specific sections of Steam is good. I won't be bringing in stuff that relates to altering Proton's normal behavior with prefixes/saves without a lot of reverse engineering on my end to match the actual Proton behavior with that.

There's other good stuff in here too, but some of the more critical stuff has already been handled in the development code I plan to push to the dev branch tonight.

You don't need to spend any time with FEX and the 'runners' until I push - I have changed how that works quite a bit, support for multiple simultaneous versions of CrossOver is in.

@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

The thing is I wanted to play Deadlock with D3DM, so I made changes according to myself without thinking much, as I was doing something randomly till it worked, and thought, 'Why not PR this?' Anyways, I look forward to the development of this project, as the performance in the game seems to be better, as Steam is native and nothing else is taking performance!

@NotProtonNot

NotProtonNot commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Could I get this PR resubmitted with specifically the prefix settings/wine reg changes + the bridge staging copying on every launch + the icon changes but without the other components?

I don't want to do the automatic prefix rebuild stuff until I've done a lot more research. That could be very dangerous. I have already dealt with the FEX/Rosetta versions being switchable inside of Steam in the dev branch/in the next update. I am not ready to review the Compatibility UI changes, but I do want those - just in a separate PR.

I've played Deadlock against Rosetta CrossOver in the most recent public and the current dev branch without an issue.

I'm sorry for the poor communication, i didn't see this PR until fairly late today.

@Ghostkwebb
Ghostkwebb force-pushed the feat/crossover-preview-dual-arch-and-speedup branch from dd73919 to 327dfb0 Compare September 30, 2026 11:06
@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Understood! Rebased and scoped the PR strictly to the launch speedups:

  • Prefix settings / registry fast-path
  • Bridge staging copy check (cmp -s)
  • Launcher icon caching and bundle reuse

Stripped out the runner selection, save sharing, and UI changes to avoid conflicts with your upcoming changes.

Does this look good, or did I miss anything?

@LaganYT LaganYT left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for rescoping this, @Ghostkwebb, and thanks @NotProtonNot for the context. I'm going through PRs to help keep code quality and reliability up, so I built and tested the new version of this one (327dfb0, now just compat_run.sh + the anchorcheck.c one-liner).

Build/tests (arm64 macOS, compared against origin/main c09e0c4):

  • make all is clean and sh -n passes. All make checks pass: compatcheck, compatsvc-check, sigcheck, anchorcheck, sigdb-fixtures, peicon-fixtures, webpatch-fixtures, spawn-env, panel-behavior, ntdll-resolve, overlay-shim-tests.
  • Heads-up for main: make sigdb-fixtures currently fails on main (the if (is_live) bad++ change already landed there). The anchorcheck.c line in this PR fixes it, so that part is worth keeping whatever happens to the rest.
  • Swift: identical to main. The same 15 tests fail on both because they need the full app payload (make app-payload). The earlier ntdll staging failure is gone now that the FEX changes are out.

Review: the three pieces NotProton asked for look right. The bridge cmp -s check is a straightforward win, and the 404 marker fix works. I checked one concern and it doesn't hold: CrossOver's wine.inf sets AeDebug\Auto with the NOCLOBBER flag, so a prefix update won't undo the one-time Auto=0. The inline comments are mostly about reliability over time: bundle reuse not noticing template changes, the setup marker being written after a failed init, the lsregister log line, and a question about dropping the per-launch wineserver -k.

Small thing: the PR title and description still describe the original scope (FEX selection, UI changes, prefix sync, "366/366 passing"). Updating them to the three launch-speed changes would make the history clearer.

Comment thread dylib/feats/compat_run.sh
chmod +x "$loader_macos/launcher"
else
WINELOADER="$loader_macos/wine"
echo "reusing existing staged bundle (skipped rebuild and lsregister)" >> "$log" 2>&1 || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log line (and the PR description's "skip … LaunchServices re-registration") doesn't match what happens: lsregister -f "$loader_app" further down (just before open -n -W) still runs unconditionally on every launch. Either gate it on bundle_needs_build = 1 to get that saving, or drop "and lsregister" from the message so the log doesn't mislead anyone debugging a launch.

Comment thread dylib/feats/compat_run.sh
echo "launcher tile hidden by NOTPROTON_HIDE_LAUNCHER_TILE" >> "$log" 2>&1 || true
fi

bundle_needs_build=0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundle reuse doesn't notice template changes. The reuse check covers the wine loader, the icon and LSUIElement, but not the generated launcher script or the rest of Info.plist. If a future NotProton release changes the LAUNCHER heredoc (e.g. the overlay-shim / DYLD_INSERT_LIBRARIES handling) or adds a plist key, existing per-game bundles keep the old versions until the CrossOver loader binary happens to change.

One cheap way to make this reliable: render the plist and launcher into variables (or temp files) first, then rebuild when either differs from what's on disk (cmp -s). Alternatively, write a version stamp into $loader_root and bump it whenever the templates change.

Comment thread dylib/feats/compat_run.sh Outdated

"$WINELOADER" reg add 'HKLM\Software\Classes\steam' /v 'URL Protocol' /t REG_SZ /d '' /f >> "$log" 2>&1 || true
"$WINELOADER" reg add 'HKLM\Software\Classes\steam\shell\open\command' /ve /t REG_SZ /d '"C:\Program Files (x86)\Steam\steam.exe" "%1"' /f >> "$log" 2>&1 || true
: > "$setup_marker" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup marker is written even if wineboot --init or the reg add calls failed, since they're all || true. If first-time init fails (e.g. wineboot times out or the runner is broken at that moment), every later launch takes the fast path and the prefix never gets initialized or gets its AeDebug / URL-protocol keys. Suggest only writing the marker when wineboot --init exits 0 (and maybe the reg adds too), so a failed init is retried on the next launch.

Comment thread dylib/feats/compat_run.sh Outdated

if [ ! -f "$setup_marker" ]; then
echo "=== first-time prefix initialization ===" >> "$log" 2>&1 || true
"$WINESERVER" -k >> "$log" 2>&1 || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: wineserver -k used to run on every launch and now only runs on first init. Normal exits and the termination trap still run kill_wine_prefix, so this only matters if the run script itself dies hard (Steam crash, force quit, SIGKILL). In that case a leftover wineserver survives to the next launch. That's probably fine with a single runner. But with the multiple simultaneous CrossOver versions @NotProtonNot mentioned, a stale server from a different wine version could give a version-mismatch failure. It might be worth keeping a cheap -k (or a prefix_game_running check) on the fast path.

Comment thread dylib/feats/compat_run.sh Outdated
if [ ! -s "$ico" ] && [ ! -f "$absent" ]; then
url="https://shared.fastly.steamstatic.com/community_assets/images/apps/$app_id/$meta_clienticon.ico"
code=$(curl -fsL --connect-timeout 5 --max-time 20 -w '%{http_code}' -o "$ico.new" "$url" 2>>"$log")
code=$(curl -sL --connect-timeout 2 --max-time 4 -w '%{http_code}' -o "$ico.new" "$url" 2>>"$log" || echo "none")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the timeouts dropped from 5s/20s to 2s/4s. On a slow connection, a larger .ico may never finish in 4s. Since only a 404 writes the .absent marker, it will retry, and time out, on every launch, adding up to 4s each time. That's still better than 20s, but a slightly longer --max-time (or a short-lived .retry-after marker after a timeout) would avoid paying it forever.

@Ghostkwebb
Ghostkwebb force-pushed the feat/crossover-preview-dual-arch-and-speedup branch from 327dfb0 to 70c3b9f Compare September 30, 2026 14:36
@Ghostkwebb Ghostkwebb changed the title Add CrossOver Preview Rosetta & FEX selection, fix launch crashes, and speed up launch times Optimize launch speed: fast prefix reuse, bridge staging check, and launcher icon caching Sep 30, 2026
@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Totally missed the template & lsregister stuff.

Just pushed an update addressing all those points, and updated the PR title and description to match the new scope!

@LaganYT LaganYT left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Ghostkwebb, nice turnaround. As before, I'm reviewing with an eye on code quality and reliability. I re-reviewed 70c3b9f, then built and tested it (arm64 macOS, against origin/main c09e0c4).

Tests: make all is clean and sh -n passes. All make checks pass, including sigdb-fixtures 28/28, which still fails on main without this PR's anchorcheck.c line. The Swift results are identical to main (the same 15 payload-dependent tests).

Previous points, verified fixed:

  • wineserver -k runs on every launch again.
  • The setup marker is only written when init succeeds (but see the inline comment; there's one catch).
  • A bundle template version now forces a rebuild, and existing bundles without the stamp rebuild once.
  • lsregister only runs when the bundle is rebuilt or its icon changes.
  • The icon fetch timeouts are now 3s/8s.
  • The title and description match the new scope.

One blocker (inline): on a fresh prefix with Retina off, reg delete RetinaMode exits 1 because the value doesn't exist yet. That marks init as failed on every launch, so the fast path never kicks in for default users. It's a one-line fix. Otherwise this looks good to me.

Comment thread dylib/feats/compat_run.sh Outdated
if [ "$NOTPROTON_RETINA" = "1" ]; then
"$WINELOADER" reg add 'HKCU\Software\Wine\Mac Driver' /v RetinaMode /t REG_SZ /d y /f >> "$log" 2>&1 || init_ok=0
else
"$WINELOADER" reg delete 'HKCU\Software\Wine\Mac Driver' /v RetinaMode /f >> "$log" 2>&1 || init_ok=0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the marker check fail for default users, so the fast path never runs. In a fresh prefix RetinaMode doesn't exist yet, and Wine's reg delete returns 1 for a missing value. I checked against the CrossOver 27.0.0.40921 runner in a throwaway prefix:

wineboot --init              rc=0
reg delete (value absent)    rc=1   reg: Unable to find the specified registry key
reg add RetinaMode           rc=0
reg delete (value present)   rc=0

With NOTPROTON_RETINA unset (the default), init_ok is always 0 here. The setup marker is never written, and every launch runs the full wineboot --init + reg add path, which removes the launch speedup for most users.

The delete just means "make sure it isn't set", so its failure shouldn't count: keep || true on this line (the fast-path delete below is already fine).

Comment thread dylib/feats/compat_run.sh
echo "launcher tile hidden by NOTPROTON_HIDE_LAUNCHER_TILE" >> "$log" 2>&1 || true
fi

bundle_template_ver=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this only works if whoever edits the PLIST / LAUNCHER heredocs remembers to bump it. A one-line comment here (and ideally next to each heredoc) like # bump when the Info.plist or launcher templates below change would help keep it reliable.

@Ghostkwebb
Ghostkwebb force-pushed the feat/crossover-preview-dual-arch-and-speedup branch from 70c3b9f to 024b84a Compare September 30, 2026 23:17
@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Is it good now?

@NotProtonNot

NotProtonNot commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

I'll be merging (and making changes - but nothing you need to deal with) shortly. I'm chasing down a minor visual bug and then I'll finally start staging pre-release stuff for the next major update.

I'll also post some contribution policy stuff and general project maintenance, to avoid issues in the event that the project grows in popularity and contributions.

@LaganYT

LaganYT commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@NotProtonNot want me to still run a code review?

@NotProtonNot

Copy link
Copy Markdown
Owner

@NotProtonNot want me to still run a code review?

I can handle it :D. Thank you, though. I'm at a point where I'm about ready to review on my end.

@NotProtonNot

Copy link
Copy Markdown
Owner

I'm starting to push to the dev branch. Check this place when I am awake and I'll continue the push, give this a full review and merge

@NotProtonNot

NotProtonNot commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Hello!

This functionality is a go for the next major release (1.1.0). I have pushed some early code to the dev branch. Ignore the messy/sloppy commit names for a few of the commits, my IDE screwed me over when I was doing the push. I'll fix it tomorrow.

How startup works has improved a lot since you began working on this/when you made the PR. If you are okay with it, I'd like to assign you authorship of the work that I have committed/will be committing that relates to that, as it was inspired by your earlier PR and would not have happened without your PR.

I would then continue reviewing and working on the other performance improvement changes you have in this PR. I think the biggest gains are in now, but I have not fully reviewed all of the stuff in this PR to see if there are further gains that would be worth any potential costs.

In short:

I agree startup can be much faster and avoiding wineboot --init on each startup shaves a lot of time off of launch.

The 'don't copy hash-identical files' feature is logical and clean, but the real world gains are near zero. Still, there is no need to replace files when they share the same hash. I'll bring it in for the release.

The icon caching and bundle reuse thing is more complicated. There are a lot of complicated edge cases with that one. I don't know that I will keep that functionality in the final shipping update. I'm going to explore it, though.

@Ghostkwebb

Copy link
Copy Markdown
Contributor Author

Yeah, totally cool with assigning authorship; appreciate it!

Glad the startup improvements are making it into 1.1.0, and I completely understand being cautious with the icon caching/bundle reuse stuff.

Well, maybe the icon caching is not saving that much time, so you can decide. I'm not sure either way XD.

Looking forward to the release!

@NotProtonNot
NotProtonNot changed the base branch from main to dev-1.1.0 October 7, 2026 00:39
@NotProtonNot
NotProtonNot merged commit 0fc5162 into NotProtonNot:dev-1.1.0 Oct 7, 2026
@NotProtonNot

Copy link
Copy Markdown
Owner

Merged the bulk of the functionality. Thank you :)

Happy to revisit the functionality that didn't make it in if you or others find the performance gains to be worth the costs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants