Skip to content

About

nxnative with a game-free shell: pack an IPA on Windows from your own NSP + prod.keys

Resources

Contributing

Stars

39 stars

Watchers

0 watching

Forks

Repository files navigation

nxnative shell — pack an IPA without a Mac

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.

Quick start (Windows)

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 ..\..\..\tools

python 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.

After the pack

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-work keeps 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.tid next to it on the first pack, so the next one does not unpack it again

Cheats (Atmosphère format)

The launcher has a Cheats section; nothing extra to install:

  1. Get a cheat file: the usual Atmosphère .txt — entries start with [Name] and carry lines like 040A0000 address value.
  2. Paste it into launcher → Cheats → "Paste cheat codes".
  3. Switch on the entries you want. The switches are remembered; clearing the text deletes the file from the phone too.
  4. Press Play. The panel writes Documents/cheats/cheats.txt and 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.

Requirements

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.

File names do not matter

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".

Which games work, and which version

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.

How it works

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>x0 sections of Frameworks/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.

Games that build their shaders at run time

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.)

Games that need one capture (no Mac involved)

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) from pipelineDB.pip. There is nothing readable to run on a PC. Three steps: pack with --no-shaders, install it, put DM_VK_CAPTURE=vkcap in Documents/env.txt and 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_CAPTURE and --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.

Games that cannot be packed yet

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.

Notes and caveats

  • 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-limit entitlement 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 inside DmIOS, 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 MB on 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:

    1. Install the IPA with SideStore as usual (the App ID is now registered under your account, without the entitlement);
    2. 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;
    3. 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 limit keeps 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's org.nxnative.shell and 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 spin in its log). The packer now reads the shell's own dm/shell_info.txt and 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=VALUE lines in the app's Documents/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.txt and 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 (in Documents/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).

Repository layout

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

Credits and license

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.

About

nxnative with a game-free shell: pack an IPA on Windows from your own NSP + prod.keys

Resources

Contributing

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages