Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 101 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 && \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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```.
Expand Down
197 changes: 197 additions & 0 deletions docs/mac-compatibility.md
Original file line number Diff line number Diff line change
@@ -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 `<style>` element added
there never reaches the page. Everything is styled on the elements themselves.
- The tile's own icon row is only shown while the tile is hovered. The badge is a child
of the tile beside that row, placed by its own style.

The anchors into Steam's interface are checked as a set before any is written. If Steam
moves one, that part switches off with a warning in `notproton.log` and the rest carries
on. The tile badge and the game page stat have separate anchors for that reason.

## Checking it

```
make tuning-check verdict-check advice-check game-check rules-check catalog-check store-check
make panel-behavior envcheck buildcheck
```

`panel-behavior` runs the injected interface code against stubs; `store-check` also runs
the store page script, and checks that it says word for word what `advice.c` says.

None of that shows what Steam actually draws. To look at the real thing, with the store
switch on, Steam's debugger is at `http://127.0.0.1:27101/json`: any DevTools client can
attach to the `Steam` window and inspect the tiles. `tools/store-badge/agent.js` injects
`store_page.js` from a script, for working on it without rebuilding the dylib.

## Known limits

- The anchors were written against Steam client build 1794536281. Other builds may need
them moved, as the existing gates do.
- The Apple mark is Apple's trademark.
- A game's badge updates when Steam starts, not the moment a session ends.
- Detection reads what is near the top of the install folder (five levels, 20,000
entries). An unusual layout can hide the real executable.
- Deck "verified" is not a promise for CrossOver, which is why it is shown as Likely.
Loading