Play your own Nintendo Switch games on iPhone and iPad. This fork of nxnative splits the build in two, so the Mac-only half happens once on a CI runner and everything that needs your game files happens on your own machine — Windows, Linux or macOS, without Xcode.
| Where | What | |
|---|---|---|
| 1. Shell | built by CI, downloaded once | A few MB of unsigned iOS app with no game content at all |
| 2. Pack | your machine | scripts/nxn_pack.py takes your NSP + prod.keys, rewrites the program, translates the shaders and writes them into the shell |
The shell is game-agnostic: one shell serves any game in the supported list. Nothing from any game ever reaches this repository or CI.
中文说明:README.zh-CN.md
Upstream project and its own documentation (the Mac route): seedds/nxnative.
Download one file: ns2ios-packer.exe — the fifth asset of the newest release. That is the whole tool. It fetches the
shell, the five executables and the packer itself from that same release, which is why the three can never fall out of
step with each other.
What the machine still needs: Python 3.9 or newer (the packer is Python; the .exe is only its window), your own game
files, and prod.keys.
| Step | What you do |
|---|---|
| 1 | Run ns2ios-packer.exe. It looks up the newest release as soon as the window opens. |
| 2 | Press Check and download. The shell zip, the five tools and the packer's source land in ns2ios-packer-data\versions\v0.0.00xx\. |
| 3 | The Python row finds your interpreter by itself; if it does not, press Browse…. Then press Install deps once — the packages go into that interpreter and nothing else. |
| 4 | Pick the game folder (the one holding the NSP/XCI), your prod.keys, and a folder for the IPA if you want one somewhere specific. |
| 5 | Press Start packing. Every step and its output appear in the log; the IPA is written when it finishes. |
| 6 | Sideload it (SideStore, Sideloadly — which drops an entitlement, see the notes — or LiveContainer). The first launch compiles the shaders once, with a progress page. |
Where does it put things? Everything downloaded lives beside the exe in ns2ios-packer-data\, one folder per release.
Nothing is installed system-wide and the registry is untouched: delete that folder and the tool is gone.
Which games work: see docs/SUPPORTED.md — every one with its title ID and the version it was verified on, so you can match the hex in your file name against it.
Prefer the command line? The window does nothing but assemble arguments for scripts/nxn_pack.py, which lives in the
source the tool downloaded. From ns2ios-packer-data\versions\v0.0.00xx\src\ns2ios-shell-*:
python scripts\nxn_pack.py ..\..\..\shell-<date>-<commit>.zip D:\games\hades2 ^
-k D:\keys\prod.keys --tools ..\..\..\toolspython scripts\nxn_pack.py --help lists every option; the window exposes the useful ones with a one-line explanation
each. The GUI never invents anything the script cannot do.
The temporary files are removed as soon as the IPA is written: the extraction, the rewritten modules, the split shaders
and the assembled app tree add up to several times the size of the game (a 4 GB game leaves ~16 GB behind) and none of it is
worth keeping. Only the few-KB log (<title ID>_pack\logs\) stays, because that is what explains a package that misbehaves
on the phone.
--keep-workkeeps that tree, so repacking the same game skips the unpack and the translation--clean <game folder>clears out what an earlier run left- a cartridge dump (
.xci) gets a<file>.xci.tidnext to it on the first pack, so the next one does not unpack it again
The launcher has a Cheats section; nothing extra to install:
- Get a cheat file: the usual Atmosphère
.txt— entries start with[Name]and carry lines like040A0000 address value. - Paste it into launcher → Cheats → "Paste cheat codes".
- Switch on the entries you want. The switches are remembered; clearing the text deletes the file from the phone too.
- Press Play. The panel writes
Documents/cheats/cheats.txtand hands the switched-on names to the game.
Two kinds of entry, and the panel marks which is which:
| Mark | Meaning | On the iPhone |
|---|---|---|
| Changes values — safe | follows a pointer and changes a number in a game object (moon jump, infinite echoes) | works |
| Patches instructions | rewrites an instruction of the game's program | crashes on device, so it starts off |
To try an instruction patch, turn on Allow code patches at the bottom of the section and switch on one entry.
Two protections: a run that dies within 30 s of starting with a set of entries on turns those entries off next launch, with the reason under each one (one crash costs one launch); and addresses the rewriter itself changed are refused outright — those words are what makes the game run on iOS at all.
Versions must match: the addresses belong to one version of the game. The panel compares the BID: in the pasted text with this build and says so in the footer when they differ.
On the packing machine:
| Need | How |
|---|---|
| The packer | Source code (zip) from the release — dm/ must be there, see above |
| Python 3.9+ | python -m pip install lz4 capstone fonttools backports.zstd pillow, into the interpreter that runs the packer |
| Five executables | From nxnative-windows-tools.zip, passed with --tools. They cover everything the packer shells out to, zstd included, so no separate utility hunt |
prod.keys |
Dumped from your own console. It never leaves your machine: it is passed to nx_extract as an argument and is not written to any log |
No .NET SDK, no C++ toolchain, no Mac. (Building the tools yourself is possible: dotnet publish dm/tools/nx_extract -r win-x64 --self-contained, same for dm/gfx/tools/dmsh_xlate; SPIRV-Cross must be the vulkan-sdk-1.4.357.0 tag.)
About Python: a machine often has more than one, and the one pip install writes to must be the one that runs
nxn_pack.py. The reliable form uses the same executable for both:
C:\path\to\python.exe -m pip install lz4 capstone fonttools backports.zstd pillow
C:\path\to\python.exe scripts\nxn_pack.py ...
The packer checks for all five packages before it starts and prints the commands to run, rather than failing halfway.
The packer identifies the game from the metadata inside the NSP — the plaintext cnmt.xml next to the (encrypted) content
files, which carries the title ID, the content type (base game / update / DLC) and the version. So files can be named
anything at all.
The [XXXXXXXXXXXXX000] and [vN] in a name are only read when a file carries no such metadata. XCI cartridges carry
neither a ticket nor a readable header (an NCA header is encrypted with a key from its own key area), so on the first pack
the packer unpacks the dump and takes the title ID from the result, recording it in <file>.xci.tid; later packs read that
instead of unpacking again.
Formats: NSP (base game, updates, DLC), XCI (base game only) and NUP (one .nup holding the game, its
updates and its DLC — no keys needed; the packer unpacks it into _nup_<title ID> beside it, and --clean removes that
again). NSZ is not supported — unpack it first with nsz -D "file.nsz".
Any game with a profile in dm/games/. Any version works, with or without an update: the packer takes the
newest version it finds in the folder (override with -p vN) and builds it with the shaders split from that version's own
files. Upstream tested each game on one version; on another version the app says so, and fixes made for the tested version
(60 fps, for instance) do not apply to it.
An update is optional — the base game alone is enough. DLC files in the folder are packed too: each one's RomFS is extracted and mounted, so the game sees exactly the add-on content you put there, and none when the folder has none, exactly as on a Switch without them.
CI (once) your machine iPhone
───────── ────────────────────────────── ────────────────────
shell_build.sh ──► nxn_pack.py
host program ├ extract NSP (nx_extract)
no game content ├ rewrite program (patch_nso.py)
no shaders ├ split shaders (shader_split.py)
no RomFS ├ translate → MSL (dmsh_xlate, spirv-cross) ──► IPA ──► sideload
├ write Guest.framework (guest_dylib.py) │
└ assemble + pack (pack_ipa.py) ▼
first launch:
compile shaders
Two things make one shell work for every game, both moved from compile time to run time:
- the module list is read back from the
__dm<name>x0sections ofFrameworks/Guest.framework(the game program is a segment-only dylib instead of being compiled into the executable); - the shader chain version is read from the name of the
msl/b-<version>directory.
The shell is also compiled against a neutral placeholder profile, so the game's memory layout (mem_used, app_pool, …) is
written by the packer into the app's dm/env.txt and read by the host at startup.
The packer works from the game's own files; a few kinds of game keep their shaders somewhere else.
A LÖVE game just packs and plays. LÖVE carries its shader wrapper as plain Lua inside the binary, so
dm/gfx/tools/gl_prebuilt.py runs that wrapper instead of the game: handed the resources/shaders/*.fs files it produces
the exact GLSL the game would compile at run time — the hashes match, because it is the same code doing the same work.
nxn_pack.py does this by itself: nothing to install, nothing to run first. (Balatro's 19 .fs files come out as 35
shaders; that is the mechanism, not a step you perform.)
A few games cannot be packed in one go: they need one run of the game on the phone first, to collect the shaders it builds at run time, and then a second pack. All of that happens on Windows plus the phone — no Mac — it just costs one extra install and a minute of play.
- Super Mario Galaxy 1 and 2 (
010099C022B96000/0100FD8022DAA000, about a minute each): their shaders are not programs in a file; they are what a GameCube/Wii GX pipeline configuration becomes, computed inside Hagi (a C++ shader compiler compiled to machine code) frompipelineDB.pip. There is nothing readable to run on a PC. Three steps: pack with--no-shaders, install it, putDM_VK_CAPTURE=vkcapinDocuments/env.txtand run the game once, then pack again with--vk-capture. Details: docs/CAPTURE.md (中文). - An OpenGL game that is not LÖVE: the same shape, with
DM_GL_CAPTUREand--gl-capture(the three steps are in the section above).
Both count as supported games, just slower. Keep the captured folder: repacking the same game needs --vk-capture
again, not another run; a different game needs its own.
Their shaders sit inside the game's own Oodle or LZ5 archives, and only the game's own decoder can unpack those — code that is Switch arm64 machine code, which only an Apple silicon (M-series) Mac can run natively. The current packer has no such step, so they fail on Windows, Linux and macOS alike. The list is the rows marked 暂不支持 in docs/SUPPORTED.md; the table below only records where each one keeps its shaders:
| Game | Title ID | Where its shaders live |
|---|---|---|
| Bayonetta 3 | 01004A4010FEA000 |
PlatinumGames PKZ |
| DRAGON QUEST XI S | 01006C300E9F0000 |
legacy Unreal pak (LZ5) |
| FINAL FANTASY RESONANCE | 0100606026F12000 |
Unreal ShaderArchive |
| OCTOPATH TRAVELER 0 | 01005270232F2000 |
Unreal ShaderArchive |
| Pokémon Legends: Z-A | 0100F43008C44000 |
Trinity packs (Game Freak) |
The packer stops as soon as it has read the title ID and loaded the game's profile — before it unpacks anything — and says which game it is and why; it never produces an IPA that installs but cannot draw. These carry a matching mark in the supported list.
Upstream's Mac route does pack these, in one run: it builds a Mac host inside the pack and unpacks the shaders with the game's own decoder — no separate capture, no phone. The difference is that it wants that machine to be an Apple silicon Mac, while the group above is the other way round: the phone captures and Windows packs, no Mac at all.
Every other game on the supported list packs and plays from its own files, LÖVE and Vulkan titles included.
-
The IPA contains your game files. It is for your own use; do not distribute it.
-
The memory limit is about 3.4 GB, and the shell asks to raise it. Without the
increased-memory-limitentitlement iOS allows a process about 3,375 MB, while the emulated game alone asks for 3,285 MiB, so a big title sits right against the wall — and going over it is a memory kill with no crash log, not a crash. The shell is signed ad hoc at build time with that entitlement insideDmIOS, because an unsigned binary has no signature for an install tool to read it from; whether it survives the re-sign depends on the tool:How it is installed Entitlement kept AltStore Classic 2.2+, Impactor, Xcode with a personal team yes LiveContainer yes (measured 2026-10-23: 6140 MBon an iPhone 16 Pro Max)SideStore with a free Apple ID yes, after a one-off fix to the App ID (below) Sideloadly no (the game runs under the 3.4 GB cap as before) How the SideStore + free Apple ID fix works (one-off; every weekly refresh keeps it afterwards): a free account can have this capability, but older SideStore builds never write it into the App ID (SideStore#1616) — the install succeeds silently, no error, the memory just stays at 3.3 GB. Fix it once:
- Install the IPA with SideStore as usual (the App ID is now registered under your account, without the entitlement);
- With the same SideStore and the same Apple ID, install
GetMoreRam, sign in under Settings (two-factor applies), open
App IDs → Refresh, find
org.nxnative.<title ID>and tap Add Increased Memory Limit. That one only: Extended Virtual Addressing is paid-account-only and the shell does not need it. If the App ID is not in the list, check that the Apple ID is the same one; - Reinstall the IPA in SideStore — over the top, do not delete it first (deleting loses the saves). The fresh profile now carries the entitlement.
Alternatively, use a SideStore build with the SideSign PR #4 fix (2026-10-05) — it works out of the box. Or skip the whole thing with AltStore Classic 2.2+ or LiveContainer, which keep it unconditionally.
To tell: the boot line in
Documents/log.txt,boot: memory limit N MB— about 3,375 MB means it did not take effect, 6–8 GB means it did. Any game shows it; there is no need for a big one, because the line is about what the process may keep, not about what the game asks for. Once the game runs,[nvn-mtl] memory up to frame N … headroom to the system memory limitkeeps reporting the remainder. -
Each game is a separate app, so several install side by side. The bundle ID comes from the title ID (
org.nxnative.<title ID>), so a second game does not replace the first and their saves are separate. Repacking the same game updates it in place, saves intact. Packages built before this change still carried the shell'sorg.nxnative.shelland did collide — repack, and delete the old one by hand. -
The shell and the packer belong together. Use the shell from the same release rather than a zip kept from earlier: an old shell may not understand what a new packer writes into it (the usual symptom is an app that dies while loading with
unknown gate kind spinin its log). The packer now reads the shell's owndm/shell_info.txtand stops at pack time with what to do, instead of leaving it for the phone to find. -
First launch takes a while: the phone compiles every shader once. Hades II's 140 shaders take about 5 s; bigger titles take minutes (the progress page estimates). The system caches the result, so later launches start immediately.
-
To change a setting on the device without repacking, put
KEY=VALUElines in the app'sDocuments/env.txt. -
Cheats (Atmosphère format): the launcher has a Cheats section. Paste the codes into it and every entry the text defines shows up as its own switch — turn on "Infinite HP" without turning on "Infinite MP". The text is written to
Documents/cheats/cheats.txtand the switched-on names to the host, so the next launch keeps both. One app holds one game, so the codes cannot be handed to another game by accident; they do belong to this version of it (the panel names the version, and absolute addresses from another version can crash the game). On the iPhone only memory values apply — the app's code pages are signed, so a code that patches instructions does nothing and says so in the log.DM_LAUNCHER=0(inDocuments/env.txt) skips the settings screen, and with it the switches. -
If it crashes, the host writes
Documents/log.txt— in almost every case that file names the reason. -
Packing needs disk space of roughly twice the NSP size: the extracted RomFS, the staged app and the IPA coexist. The temporary files are then removed automatically (
--keep-work/--clean, see above).
Only the first two entries have to exist for packing; the rest is what they depend on, plus what builds the shell.
scripts/nxn_pack.py the packer; it lives in the repository, with dm/ beside scripts/
dm/ everything packing needs -- nothing works without it:
dm/games/<title ID>/ each supported game's parameters (modules, memory layout, rewriter and shader tables)
dm/tools/ the rewriter (patch_nso.py), guest_dylib.py, nacp.py
dm/gfx/tools/ the shader chain (shader_split, dmsh_build, nometal_run…)
dm/host/ dm/ios/ host and iOS app sources (used to build the shell)
dm/ios/tools/shell_build.sh builds the shell (CI, needs the macOS 26 SDK)
dm/games/0000000000000000/ neutral profile the shell is compiled against
.shell-overlay.txt files this fork adds or changes on top of upstream
docs/SHELL.md the split in more detail
This is a fork of nxnative by seedds, and owes most of its substance to it: the rewriter, the translation chain, the host, the game profiles and the thirty-odd device-verified titles are upstream's work.
The native-execution approach itself comes from d2r-mobile by jhaizhou-ops (MIT), built for Diablo II: Resurrected. Upstream nxnative (first called smbw-mobile) started as a copy of its generic parts — the NSO rewriter, the Horizon host, the NVN-on-Metal layer and the iOS app shell — and fixes go both ways between the two projects. They are two separate projects by two different authors, each with its own game.
The shell route here follows d2r-mobile's shell_build.sh; guest_dylib.py, nometal_run.py, dm_shader_prep.m and
shell_build.sh are ported from it.
The memory limit is credited to LNPANDY/nxnative-romfs-archive
(MIT): com.apple.developer.kernel.increased-memory-limit has to be inside a placeholder ad-hoc signature, because an
unsigned binary has no signature for an install tool to read it from — declaring it in the project alone does nothing. The
measurements behind this (about 3,375 MB without it, Tears of the Kingdom killed at 3,342 MB resident) and which install
tools keep the entitlement are theirs as well. The build-time signing here (dm/ios/DmIOS.entitlements and the codesign
step in shell_build.sh) is written for this fork; no code was copied.
nx_extract uses LibHac; the shader translator embeds Ryujinx's shader library;
shaders are translated with SPIRV-Cross; zstd.exe in the tools zip is
Meta's zstd CLI v1.5.7, redistributed unmodified under its BSD licence.
License: MIT, the same as upstream. LICENSE is upstream's own file and carries the original copyright notice (© 2026 jhaizhou — nxnative kept it from d2r-mobile, whose generic parts it started from). If you redistribute this fork or a build of it, keep that notice and this license with it. The MIT license covers the code only: game files, keys and any IPA you pack are your own responsibility and are not covered by it.