# The Legend of Zelda: Link's Awakening (Mac, experimental)
A native Mac build of Link's Awakening 1.1.0 (`01006BB00C6F0000`, v131072) from the player's own
files, on a copy of the Age of Calamity target and nxnative's profile of the game
([provenance](third_party/COPIES.md)). Experimental: played by hand around Mabe Village, not yet played
through on the Mac.
## Inputs
The base game (cartridge dump or NSP) and update 1.1.0, with `prod.keys`. `scripts/extract_la.py`
chooses the programs by their content metas, checks header signatures, each program's SHA-256 and
every RomFS file through the update's hash tree, and writes `ExeFS/` and `RomFS/` to a new folder.
The build then checks that folder against `assets/expected-1.1.0.json`: the identity nxnative's
profile gives (`external`) and the files of the first checked extraction (`observed`), which say only
that a later build reads the same files.
One cartridge dump's base content meta fails its signature; `assets/integrity-exceptions.json` names
it by SHA-256. With it, the base game's version 0 is declared by an unsigned meta: it is not
authenticated, and the program is trusted through its own signature and the update's hash tree.
## Build
Apple silicon, macOS 26 or later, Xcode with its Metal toolchain, .NET SDK 10, Python 3 with `lz4`,
`capstone`, `fonttools` (and `backports.zstd` before 3.14), `spirv-cross`, `zstd` and `make`.
```sh
python3 scripts/extract_la.py --base ".xci" --update ".nsp" --keys prod.keys \
--libhac libhac.0.19.0.nupkg --work-dir "" --output "" --evidence ""
python3 scripts/build_la.py "" --work-dir "" --output ".app"
```
Work, output and evidence stay outside the checkout. nxnative records two pixel shaders of
`postprocess.bfsha` that did not translate there; `assets/shader-exceptions.json` lets exactly those two be
absent from a package, checked by their origin. It is an allowance for that case: on the Mac it was built
on, both translated, so that build did not use it.
Saves and logs go to `~/Library/Application Support/The Legend of Zelda Link's Awakening/`.
## Sharing shaders
While you play, the app keeps the shaders and pipelines it had to build on the spot in `pso_user.tsv`
beside the save folder, and builds them ahead at the next start. Settings → Shader list → **Export shader
list…** saves the ones the app's own list lacks to a file you choose (keys only, no game data). Nothing is
sent: send that file to the maintainer by hand. Exporting changes nothing in your own list.
The maintainer merges contributions into the bundled list, the current list first:
```sh
python3 scripts/pso_seed.py --output new_pso_manifest.tsv --shader-map "/source/dm/games/01006BB00C6F0000/shader_map.tsv" \
runtime/games/01006BB00C6F0000/pso_manifest.tsv la-shaders-*.tsv
```
Every row is checked, the list is never cut to fit its 1,500-line limit, and `--shader-map` refuses keys that
are not shaders of this game's 1.1.0 build. That shows the keys exist in 1.1.0, not that a file was recorded
on it: versions can share keys.
## State and work to do
Checked on an M5 Pro Mac (macOS 26.5.2). On 2026-11-07, on a disposable profile with scripted input: the
opening movie and intro, the title (L+R), a new file, Marin and Tarin's house and their conversations,
Mabe Village, and a save that loads again after quitting. On 2026-11-08, played by hand with a game
controller and sound; what was measured is in [docs/validation.md](docs/validation.md).
Fixed:
- The opening movie and the scene on the beach where Marin finds Link were out of focus and flickered: a
movie picture now stays in its slot until the game releases it. Seen right by eye.
- Age of Calamity's frame-rate control is gone: Link's Awakening paces itself at 60 FPS, and a target saved
by an earlier build is ignored (checked in a run).
- Outside Marin and Tarin's house the game ran at 60 FPS with every display tick presented (30 s of
walking). An earlier note of about 40 FPS in Mabe Village was not seen again; its cause is not known.
- Talking to Marin again: no lock-up of the dialogue was reproduced with separate presses (the box closes); a
burst of presses is compatible with the box closing and opening again (those frames were not captured).
Scripted presses used to merge into one when sent back to back; they now wait until the game sees each release.
- A game controller and the sound were checked by hand.
Known limits:
- Stutter the first time a place needs a pipeline: the app ships a warm-up seed recorded on one route
(`runtime/games/01006BB00C6F0000/pso_manifest.tsv`); places not on it still build pipelines when first
met, and players can export what their Mac built ([Sharing shaders](#sharing-shaders)). No pipeline archive.
- The shaders were compiled with this Mac's spirv-cross and Metal 32023.883, not the ones
`dmsh_chain.lock` names.
Open:
- In one session the host's display link stalled eight times (0.3 to 1.2 s) and none in its twin; the cause
is not known ([validation](docs/validation.md)).
- A changed byte inside a cartridge NCA's hashed region stops `nx_extract` with an unhandled exception
(exit 134) instead of a named exit code.
- Not checked: a play-through past the village.
- Packaging gaps against the repository's standard (the same in `games/aoc`, which this port is copied from): the shader
libraries stay in `Resources/game/shader-cache` instead of a signed `Contents/Helpers/ShaderData-*.bundle`; no
pipeline archive is built at packaging, so warm-up happens at run time and the title cannot skip it; verified digests
are not reused between steps; repackaging refuses an existing `.app` instead of keeping the folder and replacing its
contents; there is no runtime-only fast update. Not done yet.