diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index da70a25f..afca3a7f 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -122,6 +122,13 @@ jobs: make -s compatsvc-check make -s prefixstate-check make -s defaults-check + make -s tuning-check + make -s verdict-check + make -s advice-check + make -s game-check + make -s rules-check + make -s catalog-check + make -s store-check make -s compatpage-check make -s defaults-drift make -s shortcutcheck diff --git a/Makefile b/Makefile index 14db835f..e55b19da 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ CFLAGS := -arch $(ARCH) -mmacosx-version-min=$(MIN_VER) $(AVAIL) \ LDFLAGS := -arch $(ARCH) -mmacosx-version-min=$(MIN_VER) \ -dynamiclib -install_name @rpath/notproton.dylib -FRAMEWORKS := -framework CoreFoundation -framework CoreGraphics +FRAMEWORKS := -framework CoreFoundation -framework CoreGraphics -framework IOKit DOBBY_DIR := build/dobby DOBBY_LIBS := $(DOBBY_DIR)/libdobby.a \ @@ -48,8 +48,16 @@ SRCS := \ dylib/feats/webui.c \ dylib/feats/compatsvc.c \ dylib/feats/defaults.c \ + dylib/feats/machine.c \ + dylib/feats/tuning.c \ dylib/feats/compatpage.c \ dylib/feats/prefixstate.c \ + dylib/feats/verdict.c \ + dylib/feats/game.c \ + dylib/feats/advice.c \ + dylib/feats/catalog.c \ + dylib/feats/rules.c \ + dylib/feats/store.c \ dylib/feats/usershortcuts.c \ dylib/feats/conf.c \ dylib/feats/webpatch.c \ @@ -78,7 +86,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 bundlecheck launch-shell activatecheck FORCE + compatsvc-check prefixstate-check defaults-check tuning-check verdict-check advice-check game-check rules-check catalog-check store-check compatpage-check defaults-drift shortcutcheck scriptcheck envcheck buildcheck settingscheck routecheck bridgecheck gamedrivecheck launchercheck bundlecheck launch-shell activatecheck FORCE APP_PAYLOAD := app/Sources/NotProtonApp/Resources/payload @@ -194,7 +202,7 @@ PREFIXSTATE_CHECK := $(OUT_DIR)/prefixstate-check prefixstate-check: @if [ ! -f dylib/tests/prefixstate-check.c ]; then $(call SKIP,prefixstate-check,dylib/tests/prefixstate-check.c); exit 0; fi; \ mkdir -p $(OUT_DIR) && \ - $(TEST_CC) -o $(PREFIXSTATE_CHECK) dylib/tests/prefixstate-check.c && \ + $(TEST_CC) -o $(PREFIXSTATE_CHECK) dylib/tests/prefixstate-check.c dylib/feats/verdict.c && \ $(PREFIXSTATE_CHECK) COMPATSVC_CHECK := $(OUT_DIR)/compatsvc-check @@ -215,6 +223,84 @@ $(DEFAULTS_CHECK): dylib/tests/defaults-check.c dylib/feats/defaults.c dylib/fea defaults-check: $(DEFAULTS_CHECK) @$(DEFAULTS_CHECK) +STORE_CHECK := $(OUT_DIR)/store-check + +$(STORE_CHECK): dylib/tests/store-check.c dylib/feats/store.c dylib/feats/store.h $(STORE_PAGE_H) \ + dylib/util/file.c vendor/cJSON.c + @mkdir -p $(dir $@) + $(TEST_CC) -Ivendor -I$(GENERATED_DIR) -o $@ dylib/tests/store-check.c dylib/util/file.c vendor/cJSON.c + +store-check: $(STORE_CHECK) + @$(STORE_CHECK) + @if command -v node >/dev/null 2>&1; then node dylib/tests/store-page.js; \ + else echo "==> store-page: node not found, skipped"; fi + +CATALOG_CHECK := $(OUT_DIR)/catalog-check + +$(CATALOG_CHECK): dylib/tests/catalog-check.c dylib/feats/catalog.c dylib/feats/catalog.h \ + dylib/feats/advice.c dylib/feats/rules.c dylib/feats/game.c \ + dylib/feats/machine.c dylib/feats/tuning.c dylib/feats/verdict.c \ + dylib/feats/conf.c dylib/util/pe.c dylib/util/file.c + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/catalog-check.c dylib/feats/advice.c dylib/feats/rules.c \ + dylib/feats/game.c dylib/feats/machine.c dylib/feats/tuning.c dylib/feats/verdict.c \ + dylib/feats/conf.c dylib/util/pe.c dylib/util/file.c dylib/util/log.c \ + -framework CoreGraphics -framework CoreFoundation -framework IOKit + +catalog-check: $(CATALOG_CHECK) + @$(CATALOG_CHECK) + +RULES_CHECK := $(OUT_DIR)/rules-check + +$(RULES_CHECK): dylib/tests/rules-check.c dylib/feats/rules.c dylib/feats/rules.h \ + dylib/feats/conf.c dylib/feats/conf.h dylib/feats/game.h + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/rules-check.c dylib/feats/conf.c + +rules-check: $(RULES_CHECK) + @$(RULES_CHECK) + +GAME_CHECK := $(OUT_DIR)/game-check + +$(GAME_CHECK): dylib/tests/game-check.c dylib/feats/game.c dylib/feats/game.h \ + dylib/util/pe.c dylib/util/file.c + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/game-check.c dylib/util/pe.c dylib/util/file.c + +game-check: $(GAME_CHECK) + @$(GAME_CHECK) + +ADVICE_CHECK := $(OUT_DIR)/advice-check + +$(ADVICE_CHECK): dylib/tests/advice-check.c dylib/feats/advice.c dylib/feats/advice.h \ + dylib/feats/tuning.c dylib/feats/verdict.c dylib/feats/game.c dylib/feats/game.h + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/advice-check.c dylib/feats/tuning.c dylib/feats/verdict.c \ + dylib/feats/game.c dylib/util/pe.c dylib/util/file.c + +advice-check: $(ADVICE_CHECK) + @$(ADVICE_CHECK) + +VERDICT_CHECK := $(OUT_DIR)/verdict-check + +$(VERDICT_CHECK): dylib/tests/verdict-check.c dylib/feats/verdict.c dylib/feats/verdict.h + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/verdict-check.c + +verdict-check: $(VERDICT_CHECK) + @$(VERDICT_CHECK) + +TUNING_CHECK := $(OUT_DIR)/tuning-check + +$(TUNING_CHECK): dylib/tests/tuning-check.c dylib/feats/tuning.c dylib/feats/tuning.h \ + dylib/feats/machine.h dylib/feats/defaults.c dylib/feats/defaults.h \ + dylib/util/buildid.h dylib/util/file.c dylib/util/file.h + @mkdir -p $(dir $@) + $(TEST_CC) -o $@ dylib/tests/tuning-check.c dylib/util/file.c + +tuning-check: $(TUNING_CHECK) + @$(TUNING_CHECK) + COMPATPAGE_CHECK := $(OUT_DIR)/compatpage-check compatpage-check: @@ -500,6 +586,8 @@ panel-behavior: node $(PANEL_TESTS)/behavior.js $(OUT_DIR)/panel-emit && \ node $(PANEL_TESTS)/switching.js $(OUT_DIR)/panel-emit && \ node $(PANEL_TESTS)/launch-options.js $(OUT_DIR)/panel-emit && \ + node $(PANEL_TESTS)/tuning.js $(OUT_DIR)/panel-emit && \ + node $(PANEL_TESTS)/badge.js $(OUT_DIR)/panel-emit && \ node $(PANEL_TESTS)/shell.js $(OUT_DIR)/panel-emit && \ node $(PANEL_TESTS)/migration.js $(OUT_DIR)/panel-emit && \ node $(PANEL_TESTS)/localization.js $(OUT_DIR)/panel-emit && \ @@ -544,6 +632,15 @@ $(RUN_SCRIPT_H): dylib/feats/compat_run.sh dylib/embed_script.py $(OUT_DIR)/dylib/feats/compat.o: $(RUN_SCRIPT_H) +# The script the store's pages are given, carried in the dylib the same way. +STORE_PAGE_H := $(GENERATED_DIR)/store_page.h + +$(STORE_PAGE_H): dylib/feats/store_page.js dylib/embed_script.py + @mkdir -p $(dir $@) + python3 dylib/embed_script.py $< $@ np_store_page_js + +$(OUT_DIR)/dylib/feats/store.o: $(STORE_PAGE_H) + SUPPORT_DIR := $(HOME)/Library/Application Support/notproton OVERLAY_SHIM_SRC := overlay-shim/overlay_shim.m OVERLAY_SHIM_ARM := $(OUT_DIR)/overlay-shim.arm64.dylib @@ -643,7 +740,7 @@ helpers-install: $(ICONMAKER) $(APPINFO) dobby: @mkdir -p $(DOBBY_DIR) - cd $(DOBBY_DIR) && cmake $(CURDIR)/vendor/dobby \ + cd $(DOBBY_DIR) && cmake "$(CURDIR)/vendor/dobby" \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ -DCMAKE_OSX_DEPLOYMENT_TARGET=$(MIN_VER) \ -DDOBBY_DEBUG=OFF \ diff --git a/README.md b/README.md index 4f5ec728..843aadd2 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,21 @@ I would have done things differently had I been aware of that. Thanks to everyone who has positively contributed to macOS gaming. +Mac compatibility: + +NotProton works out how each game should be run on your Mac and how likely it is to run, and shows +it where Steam shows the Steam Deck's: an Apple badge on every game in the library (downloaded or +not), a stat on the game's page, and a block on store and wishlist pages. Green means it has run +well on this Mac, a green ring means Valve's Proton testing passed it, yellow means something is in +the way, red means it cannot work (an anti-cheat, usually), and a dashed ring means nothing is known. + +The graphics settings that suit the Mac and the game are applied at launch unless you set your own. +Everything can be turned off under Steam → Preferences → Compatibility. The store part uses Steam's +built-in web debugger, which opens a port on your Mac while Steam is running, and has its own switch. + +Read ```docs/mac-compatibility.md``` for what each badge means, where the answers come from, the +launch rules in ```~/Library/Application Support/notproton/rules/```, and what the store switch costs. + Developer Notes: As far as the source structure goes, the macOS app itself is located in the ```app``` folder. The core logic is in ```dylib```. diff --git a/docs/mac-compatibility.md b/docs/mac-compatibility.md new file mode 100644 index 00000000..e22dcffc --- /dev/null +++ b/docs/mac-compatibility.md @@ -0,0 +1,197 @@ +# Mac compatibility + +NotProton works out, for each game, how it should be run on this Mac and how likely it is +to run at all, and shows the answer where Steam shows the Steam Deck's. + +This page covers what you see, where the answers come from, how to turn it off, and how +the pieces fit together for anyone changing them. + +## What you see + +**A badge on every game in the library.** An Apple mark in the corner of the tile, in the +place Steam Deck badges use. Hovering it says why. + +| Badge | Label | Meaning | +|---|---|---| +| Green, filled | Verified | It has run on this Mac at least twice, for 20 minutes or more, without crashing. Or it has a native macOS version. | +| Green ring | Likely | Valve verified it, or found it playable, on Steam Deck through Proton. It has not run on this Mac yet. | +| Yellow | Playable | Something is in the way: a third-party launcher, crashes in some sessions here, or Valve marks it unsupported on Steam Deck. | +| Red | Unsupported | It cannot work here. Usually an anti-cheat that refuses Proton, or it has crashed every time it was started. | +| Dashed ring | Unknown | Nothing is known yet. | + +Filled green is kept for what this Mac has actually seen. A game that only a Steam Deck +has run gets the ring. + +**A stat on the game's page**, beside Play Time and Achievements: the same mark, with +`MAC` and the label. + +**A row on the game's Properties → Compatibility page**: "Recommended Graphics Settings", +naming this Mac, with a "Use Recommended" button that writes the settings into the game's +launch options. + +**A block on store pages and a chip on each wishlist row**, above Steam Deck +Compatibility. See [The store and the wishlist](#the-store-and-the-wishlist): this part +works differently from the rest and has its own switch. + +## Settings that apply themselves + +At launch, a game that has been told nothing gets the settings worked out for it. Anything +you set yourself wins: launch options first, then Default Compatibility Settings, then +these. + +From the Mac (`dylib/feats/machine.c`, `tuning.c`): + +- DXMT on Apple silicon, DXVK on Intel. +- MSync on, the Metal HUD off, High Resolution on for Apple silicon. +- AVX2 advertised to Rosetta on macOS 15 and later. Reduced x87 precision on FEX. +- A MetalFX upscale factor from the display's pixels per GPU core: none where the GPU + covers the panel (a 40-core Max on a 16" display), up to 2x where it does not (a base + chip on a 5K display). One band higher with 8 GB of memory. +- A frame cap at the display's refresh rate. + +From the game (`dylib/feats/game.c`, `advice.c`), read from its installed files: + +- Direct3D 12 → D3DMetal. Direct3D 11 → DXMT. Direct3D 9 → DXVK. Vulkan and OpenGL are + left to CrossOver. +- A Unity game that asks for Direct3D 12 is passed `-force-d3d11` and run on DXMT. An + Unreal one is left on D3DMetal, since forcing it to Direct3D 11 costs features. +- A game that needs Direct3D 12 on a FEX tool is flagged: FEX has no D3DMetal. + +Toggles whose right answer depends on the game rather than the hardware (DLSS, for +instance) are never set for you. + +## Where the answers come from + +Nothing is fetched from the network for the library. In order of weight: + +1. **This Mac's own history.** The run script records how each session ended + (`notproton-sessions` beside the game's prefix): healthy, crashed in the first minute, + or neither, and for how long. +2. **The game's files.** The executable's PE header and import table (word size, which + Direct3D it loads), and whether the folder carries Easy Anti-Cheat, BattlEye, a + third-party launcher, a known engine, or a macOS app bundle. +3. **Valve's Steam Deck testing**, from the `appinfo.vdf` Steam already keeps: the + category, and whether a test failed for anti-cheat. This is what gives a game a badge + before it is downloaded. An anti-cheat that refuses Proton refuses Wine as well, so + that result is taken as it stands; a "verified" is only ever shown as Likely. + +The store and wishlist ask the store's own endpoints for the same Valve report, for games +Steam has no local record of. See below. + +## Turning it off + +In Steam, **Steam → Preferences → Compatibility**: + +- **Show Mac compatibility badges** — the library badges, the game page stat, and the + store. Takes effect at once. +- **Show Mac compatibility in the Store and Wishlist** — the store part alone. Takes + effect when Steam restarts. + +Or by hand, with empty files in `~/Library/Application Support/notproton/`: + +| File | Effect | +|---|---| +| `no-mac-badges` | No badges anywhere. | +| `no-store-badges` | No store or wishlist block, and Steam's web debugger is not turned on. | +| `no-auto-settings` | Nothing is applied at launch that you did not set. | + +## Launch rules + +The flags that get a particular game past its launcher are found one game at a time, so +they live in files rather than in NotProton. A rule names a game and the words to pass +it: + +``` +[some-game-skip-launcher] +appid = 123456 +option = -skipLauncher +note = Started without the launcher. +``` + +Put rules in `~/Library/Application Support/notproton/rules/*.conf`. A rule can match on +`appid`, `engine` (`unreal`, `unity`), `launcher` (`ea`, `ubisoft`, `epic`, `rockstar`) or +`exe`; everything it names has to match. `option` may repeat. `note` is added to the +badge's tooltip. A rule that names no game, or carries shell punctuation in an option, is +refused. The format and a worked example are in `examples/rules/`. + +NotProton ships one rule's worth of behaviour itself (`-force-d3d11` for Unity on +Direct3D 12) and one example (`-nostartupmovies` for Unreal). It ships no launcher +bypasses, because a flag that helps one game can stop another from starting. + +## The store and the wishlist + +Store pages are Valve's, served from the network. Nothing NotProton patches on disk +reaches them, and Steam's client has no way to run a script in them. The one way in is +the debugger built into Steam's web helper (CEF), which is how Decky and Millennium reach +the same pages. + +So, when the store switch is on: + +- NotProton starts Steam's web helper with `--remote-debugging-port=27101 + --remote-debugging-address=127.0.0.1` (`dylib/hooks/hook_spawn.c`). +- A thread in the dylib (`dylib/feats/store.c`) connects to that port, checks that what + answers is Steam, and keeps `dylib/feats/store_page.js` registered on pages under + `https://store.steampowered.com/`. +- That script adds the Mac block above the Deck block on app pages, and a chip on each + wishlist row. It reads the page's own anti-cheat notice and macOS icon, the badges this + Mac has worked out, and, for a game it has nothing on, the store's own Deck report and + platform list (same origin, two requests at a time, remembered for the session). + +**What this costs.** The port listens on this Mac only, not the network. But any program +running on this Mac can connect to it while Steam is open, and through it can read and +drive Steam's web views, the logged-in store among them. That is a real thing to agree +to, which is why it is a separate switch and says so. With the switch off, the helper is +started as Steam starts it and no port is opened. + +## How the pieces fit + +| File | Does | +|---|---| +| `dylib/feats/machine.c` | Reads the Mac. The chip name and GPU core count come from the I/O registry, because Steam runs under Rosetta where `sysctl` reports a virtual CPU. | +| `dylib/feats/tuning.c` | Mac → settings, as lines the defaults whitelist accepts. | +| `dylib/feats/game.c` | Installed folder → what the game is. Maps the executable rather than reading it. | +| `dylib/feats/verdict.c` | A game's recorded sessions → a tier. | +| `dylib/feats/advice.c` | Mac + game + history → settings, launch arguments, badge and reason. Also the verdict for a game that is not installed, from Valve's data. | +| `dylib/feats/rules.c` | Launch rules, on the plugin grammar (`conf.c`, kind `NP_CONF_RULE`). | +| `dylib/feats/catalog.c` | One pass over the library: scans what changed, writes the badge file and each game's advice, hands the map to the UI. | +| `helpers/appinfo.swift` | `--compat` prints Valve's category and anti-cheat result per game. | +| `dylib/feats/webpatch.c` | Draws the tile badge, the game page stat, and the Preferences rows. | +| `dylib/feats/store.c`, `store_page.js` | The store and wishlist. | +| `dylib/feats/compat_run.sh` | Records sessions; applies advice and rule arguments at launch. | + +Two things worth knowing before changing the UI code: + +- Steam runs its JavaScript in a hidden `SharedJSContext` and draws into a separate + window. `document` in injected code is the hidden one, so a `