Skip to content

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

  1. Build the tools. On the Mac: cargo build --release -p napple-cli (binary target/release/napple), then the libraries and the runtime in the order tools/ci_build.sh uses: make -C input && make -C audio && make -C frontend && make -C gfx lib adapter && make -C runtime (opus and libvpx from Homebrew are linked statically). The runtime binary is runtime/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.
  2. 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 --firmware the build works and records a compat: note in napple-app.json.
  3. 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 with runtime/src/services/services.c and 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).
  4. 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
    
  5. It writes <out>/<titleid>/ holding napple-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>/ and system/fonts. The last stdout line is RESULT <path>; progress is PROGRESS <stage> <percent> <message>. <buildid16> is the first 16 hex digits of that version's own main ModuleId; for an updated game run the newest version directory (module-format.md).
  6. --updates none builds 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.
  7. Disk: the default --update-romfs materialize writes one full patched RomFS per update (Mario Kart 8 Deluxe: about 19 GB). --update-romfs in-place (needs --data-only) writes a romfs.map of about 250 MB instead and the runtime reads the RomFS straight from the game files (titles/README.md). --update-romfs delta writes a BKTR delta layer. --keep-cache keeps <out>/.napple-cache between runs.
  8. game.json in the version directory is optional and is not written by napple 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.
  9. The legacy script tools/prepare_game.sh and tools/boot_test.sh use work/games/<titleid>/<buildid16> as well; boot_test.sh <titleid> [seconds] builds the runtime, runs the newest build dir headless and writes the compat stage file work/compat/<titleid>/<build>/runtime-boot.json.
  10. 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; -v or NAPPLE_VERBOSE=1 logs every IPC command. A good run exits 0 after time limit of 45 s reached, prints no CRASH and no FAIL:, 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 a FAIL: (runtime_fail), 128+signal is a CRASH. Use a fresh --save-root; put real fonts into <save-root>/system/fonts for text (napple build --dump-fonts <firmware> -k <keys> -o <dir>), otherwise the placeholder font is served.
  11. Read what stopped it (details: debugging.md).
  12. FAIL: IPC service "<name>" not implemented or FAIL: IPC command <service>:<id> not implemented -> add-ipc-service.md. Rerun with NAPPLE_IPC_LENIENT=1 to enumerate several in one run.
  13. FAIL: unimplemented svc 0x.. (<Name>) -> add-svc.md; NAPPLE_SVC_LENIENT=1 to enumerate.
  14. [napple] CRASH signal=... pc=<module>+0x<offset> with registers and a frame chain; FAIL: ... Break for a guest abort (svc Break is reported with a guest backtrace).
  15. [napple-gfx] warn: unimplemented NVN entry point called: <name> -> add-nvn-function.md; shader ... could not be translated -> add-maxwell-shader-op.md.
  16. nvdrv: unimplemented ioctl 0x.. on /dev/...: not fatal, listed in missing_ipc of the summary as nvdrv:ioctl <path> <cmd>; implement it in runtime/src/services/nvdrv.c if the title waits on it.
  17. Frames stay 0: set NAPPLE_PCPROF=1 and 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).
  18. What to check first, in this order (cheapest first):
  19. Services and the NPDM: the npdm: start-up line (<n> services, <n> svcs allowed) and napple compat npdm. Missing service or command is the most common first blocker (missing_ipc in the summary).
  20. Applet and clock basics: appletOE (focus message, ICommonStateGetter), time:u, set, fsp-srv save-data calls, acc:u0. Loops of WaitSynchronization(handle 0) -> 0xe401 point at a wrong reply shape (a copy handle expected).
  21. nvdrv: nvmap, nvhost-ctrl and the host1x channels are partial; an unlisted device or ioctl is refused. nv::InitializeGraphics runs natively against the runtime's nvdrv, so a failure here stops graphics start-up.
  22. NVN version and entry points: the NVN API version check (NVN API version M.m detected in the main module, or game.json); then unimplemented entry points and shader translation failures.
  23. Rendering and pacing: only after frames flow. Use screenshots (NAPPLE_SCREENSHOT), then symptoms.md "Rendering".
  24. 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.
  25. Write it down.
  26. 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 to docs/titles/README.md.
  27. A new symptom -> docs/symptoms.md.
  28. Compatibility record, below.
  29. A change note docs/archive/changes/<date>-<branch>.md with a line in docs/archive/changes/README.md, as the process page asks (process/README.md).
  30. python3 tools/check_docs.py must 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-place and 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. Read default or 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-runtime also matches other sessions' processes: include the data directory in the pattern and never pkill by 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 untested or partial with the reason, not pass.
  • Services written from memory are unverified until a title uses them (runtime-services.md); say which title exercised which command.