Bring up support for a new game¶
Goal¶
Take a game you own (an NSP or XCI, with its keys, optionally its update, DLC and the console firmware), build a runtime data directory from it with napple build, boot it headless,
find what stops it, fix that in the runtime, and record the result in the repository (a title page and the compatibility matrix) without committing anything that is the game's.
Related: debugging.md (crash triage), testing.md, symptoms.md, the four existing title pages in titles/,
and the CLI reference cli.md.
Never commit¶
Game containers (*.nsp, *.xci, *.nsz, *.nca, *.nso), *.keys, firmware zips, romfs.bin, RomFS maps, data directories, screenshots that show game content in bulk, memory dumps.
.gitignore covers *.keys, *.nsp, *.xci, *.nsz, *.nca, *.nso, *.romfs, Firmware*.zip, Testing Material/, work/ and build outputs; do not rely on it for other formats.
A romfs.map (in-place mode) holds content decryption keys and absolute host paths: development only, never ship or commit it (module-format.md).
Signatures are the only per-build data allowed in the repository, under strict limits (add-signatures.md).
Files you will touch¶
| Path | What |
|---|---|
runtime/src/services/*.c, runtime/src/kernel.c, runtime/src/threads.c, ... |
Fixes for what the title needs: add-ipc-service.md, add-svc.md. |
gfx/src/api_*.c, gfx/src/shader/maxwell_translate.c |
add-nvn-function.md, add-maxwell-shader-op.md. |
docs/titles/<TITLEID>.md |
New: status of the title (see below). Name = the 16-digit upper-case application title id. |
docs/titles/README.md |
Add one row to the table at the top (the page is how the new title page stays reachable from docs/index.md; tools/check_docs.py fails on orphans). |
docs/symptoms.md |
Add a row per new symptom you root-caused: four cells (Symptom, Cause, Fix, Found on); the checker rejects rows with an empty cell. |
compat/matrix.json, COMPATIBILITY.md |
Generated by napple compat; metadata only. |
tools/check_docs.py |
Only if the new title should be required like the four reference ones: add its id to the TITLES list. |
Steps¶
- Build the tools. On the Mac:
cargo build --release -p napple-cli(binarytarget/release/napple), then the libraries and the runtime in the ordertools/ci_build.shuses:make -C input && make -C audio && make -C frontend && make -C gfx lib adapter && make -C runtime(opusandlibvpxfrom Homebrew are linked statically). The runtime binary isruntime/build/napple-runtime. The Rust workspace tests pass on Linux (cargo test --workspace, measured), so the build pipeline can be worked on there; the runtime and everything under it only build on macOS. - Check keys and firmware.
napple build --check -k <prod.keys> [--firmware <Firmware.zip|dir>] [--json]validates them and never prints key values. The firmware is only needed for the shared system fonts (pl:u); without--firmwarethe build works and records acompat:note innapple-app.json. - Look at the game.
napple plan <files...> -k <prod.keys>lists base, updates and DLC;napple ingest <game> -k <prod.keys>;napple compat npdm <game.nsp|xci> -k <prod.keys>prints the process name, 64-bit or not, main stack, and the services the title uses and hosts. Compare that list withruntime/src/services/services.cand the registrations (grep -rn ipc_register_service runtime/src): every name you cannot find will stop the boot. A 32-bit (AArch32, "NTB2") title takes the interpreter or the lifted jit path (runtime/src/a32/, titles/0100152000022000.md). - Build the data directory. For development use data-only mode:
target/release/napple build <nsp/xci files or a folder> -k <prod.keys> --firmware <Firmware.zip> --updates latest --dlc all --data-only -o work/games - It writes
<out>/<titleid>/holdingnapple-app.json,default(one line: the newest version's directory name), one directory<buildid16>/per base and selected update (manifest.json,main.npdm,modules/<name>.patched.bin+.tables,romfs.bin,nro/for run-time modules),dlc/<titleid16>/v<ver>/andsystem/fonts. The last stdout line isRESULT <path>; progress isPROGRESS <stage> <percent> <message>.<buildid16>is the first 16 hex digits of that version's ownmainModuleId; for an updated game run the newest version directory (module-format.md). --updates nonebuilds the base only;--updates all|latest|<v,v>;--dlc all|none|<idhex,idhex>. Run the command again to rebuild: existing entries in the target directory are replaced.- Disk: the default
--update-romfs materializewrites one full patched RomFS per update (Mario Kart 8 Deluxe: about 19 GB).--update-romfs in-place(needs--data-only) writes aromfs.mapof about 250 MB instead and the runtime reads the RomFS straight from the game files (titles/README.md).--update-romfs deltawrites a BKTR delta layer.--keep-cachekeeps<out>/.napple-cachebetween runs. game.jsonin the version directory is optional and is not written bynapple build:{"title_name": ..., "nvn_major": ..., "nvn_minor": ...}overrides the NVN API version the runtime would otherwise detect in the main module (runtime/src/main.c: load_game_config,detect_nvn_version). Create it by hand if graphics start-up is skipped silently.- The legacy script
tools/prepare_game.shandtools/boot_test.shusework/games/<titleid>/<buildid16>as well;boot_test.sh <titleid> [seconds]builds the runtime, runs the newest build dir headless and writes the compat stage filework/compat/<titleid>/<build>/runtime-boot.json. - Boot headless. From the repository root:
mkdir -p /tmp/napple-save runtime/build/napple-runtime work/games/<TITLEID>/<buildid16> --headless --seconds 45 --save-root /tmp/napple-save --summary /tmp/napple-boot.json 2> /tmp/napple-boot.log--trace <file>writes JSON-lines events;-vorNAPPLE_VERBOSE=1logs every IPC command. A good run exits 0 aftertime limit of 45 s reached, prints noCRASHand noFAIL:, and the gfx line[napple-gfx] stats: frames N ...shows N in the thousands (the four reference titles reach 1100-2700 frames in 45 s). Exit status 70 is aFAIL:(runtime_fail), 128+signal is aCRASH. Use a fresh--save-root; put real fonts into<save-root>/system/fontsfor text (napple build --dump-fonts <firmware> -k <keys> -o <dir>), otherwise the placeholder font is served. - Read what stopped it (details: debugging.md).
FAIL: IPC service "<name>" not implementedorFAIL: IPC command <service>:<id> not implemented-> add-ipc-service.md. Rerun withNAPPLE_IPC_LENIENT=1to enumerate several in one run.FAIL: unimplemented svc 0x.. (<Name>)-> add-svc.md;NAPPLE_SVC_LENIENT=1to enumerate.[napple] CRASH signal=... pc=<module>+0x<offset>with registers and a frame chain;FAIL: ... Breakfor a guest abort (svc Breakis reported with a guest backtrace).[napple-gfx] warn: unimplemented NVN entry point called: <name>-> add-nvn-function.md;shader ... could not be translated-> add-maxwell-shader-op.md.nvdrv: unimplemented ioctl 0x.. on /dev/...: not fatal, listed inmissing_ipcof the summary asnvdrv:ioctl <path> <cmd>; implement it inruntime/src/services/nvdrv.cif the title waits on it.- Frames stay 0: set
NAPPLE_PCPROF=1and look at the hottest (thread, pc) pairs; a thread spinning in the SDK usually waits on something a service or the HID writer did not provide (the Animal Crossing HID case is in symptoms.md). - What to check first, in this order (cheapest first):
- Services and the NPDM: the
npdm:start-up line (<n> services, <n> svcs allowed) andnapple compat npdm. Missing service or command is the most common first blocker (missing_ipcin the summary). - Applet and clock basics:
appletOE(focus message,ICommonStateGetter),time:u,set,fsp-srvsave-data calls,acc:u0. Loops ofWaitSynchronization(handle 0) -> 0xe401point at a wrong reply shape (a copy handle expected). - nvdrv:
nvmap,nvhost-ctrland the host1x channels are partial; an unlisted device or ioctl is refused.nv::InitializeGraphicsruns natively against the runtime's nvdrv, so a failure here stops graphics start-up. - NVN version and entry points: the NVN API version check (
NVN API version M.m detected in the main module, orgame.json); then unimplemented entry points and shader translation failures. - Rendering and pacing: only after frames flow. Use screenshots (
NAPPLE_SCREENSHOT), then symptoms.md "Rendering". - Scripted input. To get past a title screen,
NAPPLE_INPUT_SCRIPT="10:A,12:PLUS:0.3"(see debugging.md); the four reference pages list scripts that reach their menus. - Write it down.
docs/titles/<TITLEID>.md, in the style of the four existing pages:# <Name> (<id>),**State:**one line saying what was measured and how (headless or played, which build),## What it needs,## What is open,## Build and test(exact commands, expected frames and exit),## Numbers. Use the status words of the project: verified (ran on a Mac), tested (unit test passed), unbuilt, untested. Do not write that anyone "played" it unless a person did. Add the row todocs/titles/README.md.- A new symptom ->
docs/symptoms.md. - Compatibility record, below.
- A change note
docs/archive/changes/<date>-<branch>.mdwith a line indocs/archive/changes/README.md, as the process page asks (process/README.md). python3 tools/check_docs.pymust pass (links, anchors, orphans, symptoms rows).
Compatibility matrix (compat/, COMPATIBILITY.md)¶
compat/matrix.json is the committed record, rendered into COMPATIBILITY.md (do not edit that file by hand). Stage files live under work/compat/<TITLEID>/<BUILDID16>/<stage>.json and are described in
compat-format.md; stages: ingest build-plan nso-load site-scan rewrite rewrite-verify runtime-boot video audio input saves title-screen ingame; statuses: untested pass partial fail blocked.
- napple compat run <folder> -k <prod.keys> scans the containers in the folder, writes the first four stages and adds the game and its versions to the matrix and to COMPATIBILITY.md
(defaults: stage files in work/compat, --matrix compat/matrix.json, --md COMPATIBILITY.md). The game must be in the matrix before set works.
- The runtime writes runtime-boot.json itself with --summary <path> (or NAPPLE_SUMMARY), with counters.unsupported_svcs, missing_ipc, stubbed_ipc, implemented_ipc, missing_nvn, crashes, notes;
NAPPLE_EVIDENCE fills its evidence field. Put it at work/compat/<TITLEID>/<BUILDID16>/runtime-boot.json.
- napple compat render merges the stage files into the matrix and re-renders. napple compat set <titleid> <stage> <status> "<reason>" [--build <id16>] [--evidence <path under work/>] records a judgement by hand
(video, audio, input, saves, title-screen, ingame); give one sentence as reason, and the evidence path. napple compat show [titleid] prints it. --build limits set to one version; without it every version is updated.
- Only metadata goes in: ids, counts, names, service names. Notes beginning [auto] are regenerated by compat run.
Worked example¶
The Mario Kart 8 Deluxe page, titles/0100152000022000.md: the data build is one command (napple build <folder> -k ... --firmware ... --updates latest --dlc all --data-only --update-romfs in-place -o <out>),
the run is napple-runtime <out>/0100152000022000/fe941ed5ba14be5d --headless --seconds 120 with a NAPPLE_INPUT_SCRIPT, the expected result is stated (exit 0, 5362 frames in 120 s), and every blocker found on the way
has a row in symptoms.md (Mii default CharInfo as UUID v4, time:u cmd 300, device and account saves, the A32 main-stack reservation).
Animal Crossing's TIPC sm refusal reply and IApplicationFunctions cmd 210 show the typical "boot blocker -> one service fix -> symptom row" path
(2026-10-24-ac-boot-spin, 2026-10-24-guest-handle).
Common pitfalls¶
- Disk. Do not materialise a 19 GB RomFS for a smoke test; use
--update-romfs in-placeand delete scratch data afterwards. - Wrong directory. For an updated title the base and the update have different
<buildid16>directories; the base boots the old version. Readdefaultor take the newest. - Shared GPU. Only one game process should use the GPU at a time; the developer scripts take a lock directory
work/gpu.lock(tools/dev/README.md).pgrep -f napple-runtimealso matches other sessions' processes: include the data directory in the pattern and neverpkillby name (symptoms.md, Services and input). - zsh and
$t:A. In a script string"..:$t:A"becomes a path: write the script literally (symptoms.md). - Saves. Base and update share one title save directory on purpose; device and account saves are mounted separately (
save/space1/device,save/space1/account/<uid>). Save data is transactional: a crash discards uncommitted changes. - Do not claim more than you ran. A headless boot says nothing about audio or input quality; set those stages to
untestedorpartialwith the reason, notpass. - Services written from memory are unverified until a title uses them (runtime-services.md); say which title exercised which command.