60 fps on a 144 Hz screen cannot be paced evenly: the display link counts a tick on the refresh nearest each 1/60 s slot (link_range's virtual ticks), so frames stay up two and three refreshes in turn (13.9 / 20.8 ms) and Apple's HUD shows the frame interval jumping between them. link_sync now tells the front end the link's rate (60, 30, or 120 with DM_PRESENT_LINK=2; front_screen_match), and in fullscreen front_mac.m switches a screen whose mode is not a multiple of it to the mode of the same size with the highest rate that is, with kCGConfigureForAppOnly, so macOS switches it back when the host quits; leaving fullscreen or that screen switches it back at once. A window keeps the screen's rate and the virtual ticks: the switch changes the whole screen for every app and most external screens go black for a second or more. DM_SCREEN_MATCH=0 keeps the rate in fullscreen too. Changes to the PR: the switch only in fullscreen, with the way back on leaving it (the PR switched in a window too); no switch without a window (the windowless host's main thread runs the guest); the rate to match is atomic; the link takes a rate change only once its object exists, so one made while its thread starts is not lost, and sets the range directly instead of logging a pacing mode -1; the screen this host switched takes its rate from its mode, which maximumFramesPerSecond may not show yet. The link now also sets its range again when the window moves to another screen, which kept the first screen's range before. Checked on this Mac's built-in 120 Hz screen (a multiple of every link rate) with a harness that drives front_mac.m asked for 50 fps: no switch in the window, 120 → 50 Hz in fullscreen, 120 Hz again on leaving it, and 120 Hz after quitting in fullscreen; at 60 fps and with DM_SCREEN_MATCH=0 nothing switched. Hades windowed and fullscreen (DM_FULLSCREEN=1) paced at 60 as before, the unchanged host alike. nvn_mtl.m passes an iOS SDK syntax check. The link's new range after a switch has not run here (it needs a 144 or 165 Hz screen); the PR's author measured the windowed switch on a 144 Hz XB273U at 2560x1440: 120 Hz, the HUD's frame interval steady at 16.7 ms, 144 Hz again after quitting. Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
56704a0 could only carry most over unchecked; 81 files, 18.7 MB. The NSP script makes a missing one as a stale one (chain_tables now says whether both name the chain: with no Mac outputs a reuse builds again and a build stops), and no step's reuse key counts them (profile_files), so the run after they were made does not split again. build_ipa.sh stops for an NVN game whose c1_full.tsv is missing or names another chain, with the c1_guard.py command, before generating anything (the host would give every c1 its own 64 KiB). Checked on The Great Ace Attorney Chronicles from a checkout without them: the first pack made both (0 and 409 entries, as committed), the second reused the split and the iOS shaders with no table lines, a third with them deleted made them from the reused build's Mac outputs; build_ipa.sh stops on a missing and a stale c1_full.tsv. AGENTS.md's chain procedure, dm/games/README.md, dm/gfx/README.md and docs/development.md say so
nxnative
Play your own Nintendo Switch games natively on iPhone and iPad. Bring your game (its NSP files) and your prod.keys; one script turns them into an IPA that you sideload.
It is not an emulator: the game's own code runs directly on the Apple chip, with Switch graphics rebuilt on Metal. Nothing from any game is in this repository, and the project is not affiliated with Nintendo or any game publisher.
Acknowledgements
This project is built on other people's work:
- d2r-mobile (MIT) developed this native-execution approach, for Diablo II: Resurrected. nxnative started as a copy of its generic parts: the NSO rewriter, the host that stands in for Horizon, the NVN-on-Metal layer and the iOS app shell. Fixes go both ways.
- Ryujinx (MIT): its shader translator (
Ryujinx.Graphics.Shader,Ryujinx.CommonandSpv.Generator1.3.2, indm/third_party/ryujinxwith a few fixes listed in its README) turns the games' Maxwell shaders into SPIR-V. Its emulation of the kernel and system services is also a reference when the host's behaviour is in doubt. - Atmosphère: its kernel, mesosphère, is the reference for Horizon's synchronization semantics (address arbiter, condition variables).
- yuzu: Maxwell instruction encodings for the shader reference tool, and a cross-check for kernel behaviour.
- LibHac: reads NSPs and layers updates over the base game in
dm/tools/nx_extract. hactool did the first extractions. - SPIRV-Cross: SPIR-V to Metal Shading Language. glslang compiles the GLSL of OpenGL games to SPIR-V for it.
- deko3d (zlib): its texture layout size algorithm is ported in
dm/gfx/nvn/nvn_tex.c. - libvpx (VP9 movies), stb_vorbis (movie sound), the Vulkan headers and NVIDIA FLIP ship in
dm/third_partywith their licenses (list). - Capstone, lz4, fontTools and XcodeGen are used by the build.
Games
All of these are tested on an iPhone and playable:
Needs iOS / iPadOS 26 or later on an iPhone 15 Pro or newer (A17 Pro chip or later) or an iPad with an M1 chip or later: the games' textures are BC-compressed, and older iPhones can't read that format. Other games aren't supported yet. For them the script writes a survey and still tries its best to pack an IPA, guessing the game's settings; such an IPA may not start.
Pack an IPA
1. Get nxnative and its tools (once, on a Mac with Xcode and Homebrew):
git clone https://github.com/seedds/nxnative.git && cd nxnative
brew install xcodegen zstd spirv-cross glslang
brew install --cask dotnet-sdk
python3 -m pip install lz4 capstone fonttools "backports.zstd; python_version < '3.14'"
2. Put one game's NSPs in a folder: the base game (an NSP, or an XCI cartridge dump), plus its update and DLC if you have them:
~/Games/MarioWonder/
Super Mario Bros. Wonder [010015100B514000][v0].nsp
Super Mario Bros. Wonder [010015100B514800][v327680].nsp
Any version works, with or without an update: the script packs the newest version in the folder. DLC is optional too: the DLC in the folder is packed, and without it the game sees none, as on a Switch without it. Each game was tested on one version (an update named in the table above is the one its 60 fps needs). With another version the script builds the game with that version's own shaders and says that the version was not tested; fixes made for the tested version's code, such as the 60 fps ones, don't apply to it.
3. Run the script:
python3 scripts/build_ipa_from_nsp.py ~/Games/MarioWonder -k ~/keys/prod.keys
The IPA appears in the same folder (here ~/Games/MarioWonder/SMBWMobile.ipa). The first build takes 30–60 minutes, later ones about 10. You need free disk space of about twice the NSPs' size plus 10 GB. Options such as -o (output path) and --app-name are listed by -h.
4. Install the IPA with your sideloading tool (AltStore, Sideloadly, LiveContainer, …). The app gets the game's name, icon and version (the update's version, for example 1.2.1 for Mario Wonder; the base game's when there is no update). Sign it with the Increased Memory Limit capability (com.apple.developer.kernel.increased-memory-limit) if your tool can: without it iOS gives the app about 3.4 GB and closes the bigger games (Hyrule Warriors: Age of Calamity and Hades II use more, Breath of the Wild and Cuphead come close); with it a recent iPhone gives about 6 GB.
With an Apple developer account you can instead build and install from Xcode: --prepare-xcode makes an Xcode project for the game rather than an IPA (Build and run on a device with Xcode).
Play
Settings. A screen before the game starts offers what the game supports: frame rate, render scale, docked or handheld mode, game language, the frame-rate counter and Apple's performance HUD. Tap Play to start; the app remembers your choices.
Touch controls. Tap 2D / 3D at the top to switch layouts; the app remembers your choice. Wonder, Engage, Dread, 13 Sentinels, Link's Awakening, Ace Attorney Investigations Collection, Unicorn Overlord, Rhythm Heaven Groove, Octopath Traveler 0, Pokémon Legends: Z-A, Cuphead, Absolum, Brain Memory Training Academy, The Great Ace Attorney Chronicles, STEINS;GATE RE:BOOT, Big Brain Academy: Brain vs. Brain, Patrick's Parabox, Celeste and Raging Loop start in 2D, the other games in 3D (STEINS;GATE RE:BOOT's quick save is R3, in the 3D layout). In 3D, the right stick (the camera in most games) is not drawn: swipe on the right third of the screen, starting anywhere off the buttons. In Super Mario Galaxy and Super Mario Galaxy 2, other touches outside the controls move the pointer; in Captain Toad: Treasure Tracker they touch the game's screen (touch blocks, stun enemies), and in Big Brain Academy: Brain vs. Brain too (its menus and activities). A connected game controller hides the touch controls and keeps every button.
Shaking. Shake the iPhone to shake the controller, in games that read its motion: Super Mario Odyssey's motion moves (such as the cap throw on a shake) need it, and Odyssey only takes them in its default docked mode. A light bump does not count; a sharp flick of the phone does.
- Game language is English by default. Pick another when packing with
--language(-l), by code or by name:-l ja,-l zh-Hans,-l French(LANGUAGEin Spyder). It must be one the game has; the script prints the game's languages and stops on one it lacks. To change it without rebuilding, pick another on the settings screen. - Saves and logs are in the Files app, under the app's name:
save/andlog.txt. When something goes wrong, sendlog.txt(the previous run islog.prev.txt). - Short stutters the first time a new scene appears are normal: the app compiles graphics for it once and remembers them for the next launch.
- Reinstalling: some sideloading tools give a reinstalled app a new data folder. Copy
save/andpso_user.tsvover from the old one to keep your progress. - Settings in a file: lines like
DM_OPMODE=0(handheld mode: lower resolution, higher frame rate) inDocuments/env.txtwin over the settings screen, which then shows them greyed out.DM_FPS_TARGET=30keeps Breath of the Wild, Tears of the Kingdom and Octopath Traveler 0 at their original 30 fps, which saves battery;DM_PRESENT_INTERVAL=gamedoes the same for Monster Train 2.DM_SHOW_FPS=1shows the frame rate at the top left (--show-fpswhen packing turns it on from the start).DM_LAUNCHER=0skips the settings screen.
Known issues
- No amiibo and no online features.
- Fire Emblem Engage: after switching away from the app and back, some sounds may stay silent until you restart it.
- Fire Emblem: Three Houses: small square patches on the ground where decals land.
- Super Mario Galaxy and Super Mario Galaxy 2: effects drawn as points or lines come out one pixel wide.
For developers
How it works, the script's options, running games on the Mac, adding a game, and per-game notes: docs/development.md. In-depth bring-up and debugging notes: docs/agents.
License
MIT (see LICENSE). Third-party code keeps its own license; see dm/third_party.













































