Reading the repo
Reading the repo
How to install on Linux
Switch games running natively on iPhone, iPad and Mac: an NSP folder becomes an unsigned IPA (NSO rewriter, Horizon HLE host, NVN→Metal)
Saved setup from 3 h ago
Will it work on my device?
docs/development.mdThe README covers packing and playing. This page is for working on nxnative itself: how it is built, the script's options, running games on the Mac, and adding a game (per-game notes are in dm/games). Agent-oriented details (bring-up failure catalogue, graphics specifics, debugging recipes) are in docs/agents.
| Part | State |
|---|---|
| Rewriter (dm/tools/patch_nso.py) | Runs unchanged on every module of every supported game. Per-build cuts are keyed by the module's build ID, in the profile's patches.py (a game's own modules) or dm/tools/sdk_patches.py (Nintendo's sdk modules). Profiles can opt verified cuts into function matching across updates |
| Host (dm/host) | Stands in for the Horizon kernel and the system services the games call: threads and synchronization, memory, file system with transactional saves, HID, audio, movies (VP9 through libvpx, Vorbis sound through stb_vorbis), software keyboard, focus and lifecycle |
| Graphics (dm/gfx) | NVN on Metal. Shaders are extracted by shader_split.py from Nintendo's containers and from Unity's Shader objects, then translated offline. Mario: 16,266 of 16,268 shaders compile. Engage: 9,268 of 9,269. Games that render with Vulkan (Hades) or OpenGL (Digimon Story Cyber Sleuth) go through layers of their own on Metal (dm/gfx/vk, dm/gfx/gl) |
| Audio | Games that carry the SDK's audio renderer (Mario) run it in-process; games that use AudioOut directly (Engage: Wwise) feed it straight. With Core Audio enabled (default on iOS, DM_AUDIO=1 on the Mac), the silent sink keeps releasing PCM buffers while a device starts or retries. |
| Input | iOS: the touch gamepad (hidden while a physical controller is connected) and physical controllers; shaking the phone shakes the controller (its six-axis sensor, hle_hid.c; the accelerometer in DmGamepad.m, threshold DM_SHAKE_G, 1.5 G by default). Mac: scripted input (DM_KEYS=<seconds>:<button>,…, Shake for a shake, with DM_KBD=<text> answering the system keyboard; DM_REC=<file> records what the player does in that syntax, 05) or DM_WINDOW=1 for a window with keyboard input (Space shakes) and physical controllers (face buttons by position as on the iPhone, a Nintendo pad by its labels: pad_apple.h). On the Mac and iOS each controller is a player of its own, for local co-op: the first one connected is player 1, together with the keyboard (iOS: the touch gamepad and the phone's gyro), the next player 2, and so on up to 8 (pad_slots.h); a controller that is removed frees its player and gets it back when it reconnects, and the log names each one's player and npad (controller 'Xbox Wireless Controller' connected: player 2 (npad 1)). Player 1 is the Handheld npad in handheld mode and No1 docked, player n is No n; the game's supported npad ids decide how many play (hle_hid.c): a one-player game gets every controller as player 1, and a controller past a game's last player is not used. DM_PAD_MERGE=1 makes every controller player 1, as before (on iOS from Documents/env.txt); DM_KBD_PLAYER=own makes the keyboard player 1 on its own, with the controllers from player 2 on. The window goes fullscreen from its green button or ⌃⌘F; DM_FULLSCREEN=1 starts it that way. In fullscreen, a screen whose refresh rate is not a multiple of the game's (144 or 165 Hz) runs at one that is (120 Hz) until the window leaves fullscreen; DM_SCREEN_MATCH=0 keeps its rate. The drawable follows the window in pixels (fullscreen at the screen's resolution); DM_METAL_HUD=1 shows Apple's performance HUD (on the iPhone too, from Documents/env.txt). The left mouse button is the touchscreen, on the 16:9 picture. Motion: a controller with a gyroscope drives its player's six-axis sensor (motion_apple.h → dm_motion_set, hle_hid.c; orientation from the gyro with tilt correction toward gravity, motion_fusion.h), and on iOS the phone's own device motion drives player 1's while no controller supplies fresh motion (DmGamepad.m, in the Switch frame: the pad view's own frame on iOS 27 and later (deviceMotionBody), turned for the screen's orientation before that (The phone's motion in the picture's frame); DM_PHONE_MOTION=0|1|auto; 1 selects only the phone). On the Mac, a Switch Pro Controller whose GameController motion is empty can use passive HID reports instead (motion_pro_mac.h; DM_MOTION_HID=0 turns this reader off). This requires exactly one Pro in each API so it can use the same player slot without guessing; multiple Pros retain GameController input. Raw HID has its own orientation and nominal sensor scales; hardware validation of the reworked reader remains open (Pro Controller fallback). DM_MOTION_AXES / DM_MOTION_ACC_SIGN apply only to GameController. Each source keeps its own orientation. Without live motion the sensor lies flat as before. DM_MOTION_LOG=1 logs player 1's mapped sample once a second per source; DM_MOTION_AXES (e.g. x,-z,y) and DM_MOTION_ACC_SIGN=-1 remap a controller whose axes differ, DM_MOTION_GAIN sets the tilt correction (0.5), DM_MOTION=0 turns motion off (the NSP script's --no-motion builds it in). The console's own sensor (SevenSixAxisSensor, read in handheld mode) still reports no states. In handheld mode the touches also drive hid::GetGestureStates (taps, presses, pans, swipes, pinches and rotations recognized by the host, hle_touch.c) |
| iOS app | One unsigned IPA for iPhone and iPad. The rewritten code is embedded in the executable; the shader libraries, the RomFS and the game icon are inside the app. A settings screen comes before the game (Launcher.swift, drawn from dm/launcher.json, which dm/games/settings.py writes from dm/games/settings.tsv); the host's loading starts at app launch meanwhile, and the frame rate and render scale go into the frame step state when the game starts (loader.c dm_fps_state_apply) |
| Known gaps | Conditional render is always drawn; indirect-count multi-draws can wait for a GPU-written count when the records need conversion, cannot be GPU-masked, or exceed the mask table capacity; geometry shaders do not translate. TBDR optimizations are off by default (they don't understand bindless textures yet). No amiibo. On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone) |
How a Switch with Atmosphère runs cheats, and where this host differs, is in Cheats on a Switch.
Atmosphère-format cheat code files (atmosphere/contents/<title ID>/cheats/<build ID>.txt files that cheat sites share) apply to the running game (dm/host/hle_cheats.c): the file of the running build is chosen by the first 16 hex digits of the main NSO's build ID (FFCC 1.0.1: DF154E1D08E39646), from a folder holding either the flat <build ID>.txt layout, the Atmosphère tree under it, or both. The Mac window (DM_WINDOW=1) has a Cheats menu: each [Name] cheat is a checkmark row that toggles when clicked, a {Name} master code is always on and not toggleable, a cheat the VM cannot run shows its reason (unsupported opcode FFF) and stays off, and with no file of this build the menu lists No cheats for build … plus each other build's .txt in the folder as another version of the game (all disabled; DM_CHEATS_ANY_BUILD=1 loads one, with a [RED] warning). The menu also reloads the file in place (Reload cheats, ⌘R) and opens the folder (creating it when missing). Which cheats are on is remembered per build in <build ID>.enabled (one name per line) in the folder, restored at the next launch. A cheat that writes code does so on the Mac only, and never over a word the rewriter changed or moved (main.changed.tsv beside the modules, written by patch_nso.py): such a cheat turns off with the reason (what the host does differently). The menu's first item Show Cheats Window (⌘⇧C, or DM_CHEATS_WINDOW=1 at launch) opens a Cheats panel (dm/host/front_mac.m, DmCheatsPanel), a floating utility panel beside the game. A click never takes the key window from the game, so the game keeps keyboard and controllers; the panel takes the keyboard only while the player types in its search field or a value field: Return gives it back with the value applied, Escape gives it back with the typed value abandoned, and a click anywhere else in the panel never keeps it. It shows:
A .txt or .cheats.toml dropped on the panel (from a Finder window in the background: the panel hides while another app is in front), or picked in Find Cheats…, is copied into the cheats folder and the file reloaded: a build-ID name keeps its name, any other .txt (also one named after the title ID) becomes this build's after the player confirms, and a file already there is kept as .bak (replacing an older .bak). Find Cheats… lists only files named after a build ID, so Ryujinx's enabled.txt stays out. The menu shows the same rows in short, with Cheats On and, in safe mode, Restore Saved Cheats. Every click, a menu toggle and a ~1 s timer while the panel is visible refresh it. Closing the panel only hides it; its place and size are remembered. The enabled cheats run once per presented frame and at ~60 Hz from a host thread whenever the game is not presenting: one pass reads the keys once, then runs the masters, then the enabled cheats in file order, with the registers and the saved values reset once per pass and shared between the cheats, as dmnt's one concatenated program has them (a pointer chain compiled by cheat_anchor.py zeroes its register 15 behind itself for that reason); each cheat is still its own program, with its condition depth and loop tops (word indexes into its own words) starting fresh, so an unclosed block in one cannot swallow the next cheat's words, and a loop end without its start jumps to the first word of its own cheat, never into another cheat's. A failed conditional block is skipped like the reference's. An access to memory that is not mapped (a pointer the game has not set yet, at start-up or on a loading screen) is ignored as on a Switch: a load leaves the register as it was, a conditional compares against 0, a store writes nothing, and the cheat stays on and retries next pass; the first failure of a cheat logs one [RED] line, every one is counted, and the row shows (no effect: N accesses out of mapped memory) while the cheat's latest pass had one (the note goes on the first pass without). A run of hex digits is cut into 8-digit words wherever whitespace falls, as dmnt reads it (0400000000123456 is two words). A line that mixes code words with a token that is not one, or holds hex digits that are not whole words (0400000, dead beef), turns its cheat off (malformed line N): a silently dropped word could be a begin or an end whose absence leaves a body running every frame. A cheat that names the heap before the game's first SetHeapSize waits (logged once): it runs as soon as the game opens its heap. Writes into main's code pages go through a writable alias of the whole image (mach_vm_remap, same physical pages) mapped when a cheat file is picked: the game's own mapping never changes protection, so a running core cannot fetch a page mid-toggle (RWX is denied on Apple Silicon, and toggling the original page race-faults fetches, found on FFCC), and a run without a cheat file never maps one. If the alias fails to map the writes fall back to the per-page mprotect toggle with a [RED], and every code-page write is passed through sys_icache_invalidate.
Switches:
DM_CHEATS=<folder>: cheats folder. Default <DM_ROOT>/cheats (the game's work folder). A flat folder and the Atmosphère tree both work.DM_CHEATS_ANY_BUILD=1: with no file of this build, load the folder's single .txt of another build with a [RED] warning that addresses may be wrong.DM_CHEATS_ON=<name>,…: cheats turned on by name at launch (case-insensitive; the <build ID>.enabled states still apply).DM_CHEATS_DEBUG=1: log every write the cheats make (cheat name, bytes, value, guest address), and with no file of this build the other-build .txt list.DM_CHEATS_WINDOW=1: open the Cheats panel at launch.Implemented opcodes, all with the reference's word order and block-skipping (dmnt_cheat_vm.cpp):
The others (pause/resume, the debug log) mark the whole cheat (unsupported opcode N), shown disabled in the menu and never run; so does an instruction whose last words are missing (truncated), where dmnt would run the whole words and end the pass. An else at depth 0 turns its cheat off (dmnt aborts there); an end at depth 0 is a no-op; a loop that never ends (a counter of 0) is stopped by a step cap and turns its cheat off, where dmnt's VM thread stays blocked. The memory-type nybble selects dmnt's regions from the host's own layout: 0 main NSO, 1 heap (a cheat that names it before the game's first SetHeapSize waits, logged once), 2 alias (virtual address memory), 3 aslr (the whole guest block), 4 absolute, 5–F main. What an access reaches is the kernel's rule as on a Switch, restricted to the game's own memory: a read any page of the guest block or of a module (a stack, the alias region, another NSO or NRO), a write main (through its alias), the opened heap, another module's data and bss, or any other mapped guest page, never another module's code or read-only data nor the host's key page. Pages the game maps and unmaps while a pass runs (stacks, the alias region) are copied through the kernel (vm_read_overwrite, vm_write), which fail instead of faulting on a page gone in between; the heap goes through hle_svc.c's map lock as before. The state files (<build ID>.enabled, the safe-mode flag) are written by a serial dispatch queue, never on the presenting thread, in the order they were asked.
On top of the format (hle_cheats.c; the dm_cheat* calls of dm.h give them to a front end, the Mac panel above):
Labels and sections. An entry without code words is a label, not a broken cheat: a credits line, or EdiZon's [--SectionStart:Name--] / [--SectionEnd:Name--], whose entries in between carry that section. A header glued to the previous code word (00000008[Next], found in a cheat-site file) still starts its own entry. Code words on the header's own line belong to the entry, as in dmnt. An entry whose lines were all dropped as prose is broken (no code words), not a label.
On / off pairs. "X - on" / "X - off" entries (also "X on", "X (off)", …, or a pair the meta file names) are one switch. Turning the on half on turns the off half off. Turning it off runs the off half, the restore, for one pass. A saved state is kept as it is.
Duplicate names. <build ID>.enabled saves the second entry of a name as Name #2, and so on. A plain name, as older files have it, turns on the next entry of that name that is still off. One-pass cheats ("set", a pending restore) are never saved as on.
The meta file. <build ID>.meta.tsv beside the file (Cheats that survive game updates) adds per entry:
<build ID>.values);.txt holds an empty entry that other engines show as a label.The meta file also gives the display versions and lists the cheats the compiler left out, with their reasons.
Toggles of a new build. A compiled file (one with a meta file) of a build without <build ID>.enabled takes its toggles, by name, from the newest other build's <build ID>.enabled whose meta file names the same source.
Dry run. dm_cheat_preview lists what a cheat would write right now (main+0x5607AAE (2 bytes): 37 -> 999), or why nothing (a condition not met, a patch whose code is not the original any more, an access that fails). It replays the cheats a pass runs before it, so its registers are a real pass's; it writes nothing and changes no state.
Master switch. A .cheats-off-<title ID> file in the folder stops every cheat of that game, masters included, without touching the toggles.
Safe mode (Mac only: an iOS app usually ends killed). While some cheat runs, the folder holds .cheats-running-<title ID>; a clean exit (exit, or dm_host_exit before its _exit) removes it
git clone https://github.com/seedds/nxnative.git cd nxnative
We couldn't spot a known stack. Start with the README, or ask an AI below to go through it with you.
Install the IPA on your iPhone or iPad with a sideloading tool such as AltStore, Sideloadly or LiveContainer. If your tool offers Increased Memory Limit, turn it on: the bigger games need it.
Game too big to install? Build again with --romfs separate, and run the command the script prints once to copy the game files to your phone (Quick reinstalls).
Controls, saves and settings: Playing.
No reports for Linux yet.
Opens a chat with this manual already filled in, for Linux. It uses your own account; gitmanual sends nothing.