OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
luna — command reference (pinned v1.38.0)

luna is the cycle-accurate emulator every OpenSNES test runs on (.claude/rules/testing.md). The SDK pins one release in testing/luna.version; scripts/install-luna.sh fetches it and make tests uses nothing else. This page is that release's own --help, one section per subcommand, so what you read here is what the pinned binary accepts — not a newer or older luna.

How the SDK uses it: luna run for corpus liveness and visual hashes, luna state --assert for the runtime ROMs and probes, luna test for the testing/manifests/*.toml, luna diff for the Class A A/B, luna profile for measured coverage and the cycle budget, luna wram-trace behind the WRAM oracle. The MCP server (luna mcp) is the interactive debugger (docs/tutorials/debugging.md).

luna --help

SNES emulator with introspection API
Usage: luna <COMMAND>
Commands:
run Load a ROM, step the CPU N instructions, optionally dump a screenshot of the resulting PPU state
test Run manifest-driven homebrew tests: one TOML per test (rom, input, run bound, asserts), executed in-process through `luna-api`. Exit 0 = all pass, 1 = assert failures, 2 = manifest/usage errors — the CI contract. The machine is set per manifest, not by flags: `region = "pal"` (or `"ntsc"`) forces the video standard as `--force-region` does, `force_mapper` the mapper, `power_on` / `seed` the RAM the ROM boots on
mcp Serve the Luna MCP server on stdio
state Run the emulator through `luna-api` and emit a JSON state snapshot — the same data the MCP `state` tool returns
frames Capture a sequence of EXACTLY-consecutive PPU frames as PNGs in one run, via the same `luna-api` render path the GUI uses. Use this to diagnose *temporal* artefacts (flicker / double-buffer page-flip desync) that a single `state --screenshot` cannot show — it samples one frame, so a frame-to-frame "blink" is invisible to it. Each frame's PNG is tagged with its frame number and the INIDISP forced-blank flag, so you can see exactly what the GUI would (and would not) display
diff Compare two ROMs frame by frame: run both in one process, hash the displayed frame at every PPU frame, and print MATCH / DIFF for each requested frame — MATCH when A's frame `F` equals B's frame at some `F ± tolerance` (the boot-length offset a codegen change can introduce). The "compare at equal frame" protocol that validates a compiler / library change. Exit 0 = all match, 1 = any DIFF, 2 = usage error
profile Real master cycles per symbol: every step credits its cycles — bus + internal cycles plus the DMA / HDMA / refresh stalls charged during it — to the instruction's address; the report folds those onto the nearest `.sym` label (or a 256-byte page without one), heaviest first. Replaces static instruction weights with what the machine paid
wram-trace Emit per-frame (vblank-aligned) WRAM page hashes for a confound-free cross-emulator differential. Each line is `<ppu_frame> <h0> <h1> ... <hN>` where each `h` is the FNV-1a hash of one WRAM page (`--page-size` bytes, default 4 KiB → 32 pages). Because WRAM-at-vblank-N is the SAME game-frame in both luna and a reference emulator (no input ⟹ game logic advances once per NMI), the first frame whose page hash differs from the reference pins the first REAL state divergence — unlike scene- level windows the boot-frame offset confounds
bench Run every ROM in a directory headless, detect anomalies (crashes, freezes, dead APU, missing firmware), and write a compatibility report + one markdown bug file per finding. Stresses the CLI/API across the whole corpus. Reports stay local (under `--out`)
spc-dump Run a ROM until its music driver is playing, then export the APU state as a `.spc` sound file (SNES-SPC700 Sound File Data v0.30): SPC700 registers + 64 KB ARAM + DSP registers + IPL ROM, playable in any SPC player. Step far enough in (and pulse Start via `--input`) that the music has started before the snapshot
assets-dump Run a ROM to a scene, then dump every graphics asset currently loaded as PNGs: the VRAM tile sheet, the four BG tilemaps, the CGRAM palette, the OAM sprite sheet, and the composited screen — plus raw `vram.bin` / `cgram.bin` and `oam.json`. This captures the assets *loaded at this instant* (already decompressed by the game); snapshot several scenes (via `--input` / different `-n`) to cover a whole game. A static whole-ROM rip is NOT possible — SNES graphics are game-specific-compressed with no standard layout
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version

luna run

Load a ROM, step the CPU N instructions, optionally dump a screenshot of the resulting PPU state.
A panic inside the core is caught and reported — the state reached so far is still dumped.
Usage: luna run [OPTIONS] <ROM>
Arguments:
<ROM>
Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS>
Maximum number of CPU instructions to execute before dumping
[default: 64]
--until-frame <UNTIL_FRAME>
Run until PPU frame N (then dump), instead of stopping at the `-n` instruction count — same as `state --until-frame`. Pins a `--print-fbhash` / `--screenshot` baseline to a PPU frame so a codegen change that shifts the instruction count cannot move the capture onto another animation phase
--screenshot <SCREENSHOT>
If set, render the composited framebuffer as a PNG and write it to the given path: 256×224, 256×239 while the game runs in overscan, or 512×448 (512×478 in overscan) with `--native-res`
--force-display
Bypass INIDISP forced-blank when rendering. Lets you see whatever the game has uploaded to VRAM/CGRAM even if its init left the screen blanked (e.g. a title still waiting on a Start press)
--bg <BG>
If set (1..=4), render ONLY that BG layer. Default is the composited BG3-over-BG1-over-BG2 frame (right for most Mode-1 title screens)
--audio-out <AUDIO_OUT>
If set, capture the APU's stereo 32 kHz output and write it to a WAV file at the end of the run. Lets the emulator be audio-verified without a GUI / sound card
--nocash-out <NOCASH_OUT>
If set, capture the program's `$21FC` Nocash-TTY output (the SDK's `SNES_NOCASH` / `SNES_ASSERT` debug channel) and write the raw byte stream to this file. Lets a headless harness read the ROM's own log / assertion output with no GUI debugger
--wdm-out <WDM_OUT>
If set, write captured `WDM $xx` executions (the SDK breakpoint / `SNES_ASSERT` channel — `WDM $00`) to this file, one `PC=$xxxxxx operand=$xx` line per hit. A non-empty file means an assertion or breakpoint fired during the run
--print-fbhash
If set, print a hash of the displayed frame (honouring `--force-display`) to stdout as `fbhash=<16-hex>`. A stable, cross-architecture visual-regression key — it hashes the same pixels `--screenshot` writes, before PNG encoding (so it is immune to the build-dependent PNG encoder). Ideal as an external test harness's baseline key. See `docs/trace_determinism.md` for which outputs (fbhash / trace counts / WRAM bytes) are cross-arch-stable and how strongly each may be asserted
--native-res
Emit the native 512×448 frame: hi-res modes 5/6 and pseudo-512 keep their two horizontal subpixels per dot, interlace keeps both fields as separate lines — instead of the default 256×224 averaged view. Applies to `--screenshot` and `--print-fbhash`
--force-mapper <FORCE_MAPPER>
Force a cartridge mapper, bypassing header auto-detection — same as `state`/`frames`. Needed for checksum-invalid homebrew / reference ROMs (e.g. the `PeterLemon` corpus) so they can reach `--print-fbhash`. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION>
Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--power-on <POWER_ON>
What RAM holds before the ROM boots: `zero` (default), `ones`, `random` (a seed is derived and printed) or `random=<seed>` (decimal or 0x hex — replays an exact machine). Fills WRAM, VRAM, CGRAM, OAM, APU RAM and the cartridge RAM no battery keeps (Game Pak RAM, BW-RAM, I-RAM); a boot bug that only shows on real hardware's garbage RAM shows here too
-h, --help
Print help (see a summary with '-h')

luna test

Run manifest-driven homebrew tests: one TOML per test (rom, input, run bound, asserts), executed in-process through `luna-api`. Exit 0 = all pass, 1 = assert failures, 2 = manifest/usage errors — the CI contract. The machine is set per manifest, not by flags: `region = "pal"` (or `"ntsc"`) forces the video standard as `--force-region` does, `force_mapper` the mapper, `power_on` / `seed` the RAM the ROM boots on
Usage: luna test [OPTIONS] [PATHS]...
Arguments:
[PATHS]... Manifest files, or directories scanned recursively for `*.toml`. Defaults to `./tests`
Options:
--update Rewrite each manifest's `asserts.fbhash` with the measured value (regenerate goldens after an intended render change). Formatting and comments are preserved
--only <ONLY> Only run manifests whose path contains this substring
--report <REPORT> Also print a machine-readable JSON report to stdout [possible values: json]
--jobs <JOBS> Run up to N manifests at once (0 = one per CPU). Each manifest gets its own emulator; the report is the same lines in the same order, and the same exit code, as a serial run. Manifests chained through a battery file (one's `srm_out` is another's `srm_in`) still run one after another, in path order; there is no other ordering between manifests [default: 1]
-h, --help Print help

luna mcp

Serve the Luna MCP server on stdio.
Once started, Luna exposes its full tool catalogue (load / run / observe / trace — see the book's MCP section, or the `capabilities` tool at runtime) to any connected MCP client (Claude Desktop, Claude Code, custom clients). The process stays alive until the client closes the stream. With `--rom`, the session starts with the ROM (and symbols) already loaded.
Usage: luna mcp [OPTIONS]
Options:
--rom <ROM>
Preload a ROM so the client can skip `load_rom` (a `<rom>.sym` beside it is auto-loaded, wlalink-style)
--sym <SYM>
Explicit WLA-DX `.sym` to load after the ROM (overrides the beside-ROM auto-detection)
--force-mapper <FORCE_MAPPER>
Force the mapper for `--rom` instead of header auto-detection. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION>
Force the video standard for `--rom` (ntsc, pal)
-h, --help
Print help (see a summary with '-h')

luna state

Run the emulator through `luna-api` and emit a JSON state snapshot — the same data the MCP `state` tool returns.
This is the dogfood path: the API surface that the CLI, GUI and MCP server all share. Use it to test the API directly without going through any transport.
Usage: luna state [OPTIONS] [ROM]
Arguments:
[ROM]
Path to the .sfc / .smc ROM file
Options:
--schema
Print the JSON Schema of the state JSON (`EmulatorState`, plus the `peeks` array `--out` adds) to stdout and exit — no ROM needed. The machine-readable map of every nested field a harness can `jq`
-n, --steps <STEPS>
CPU instructions to execute before snapshotting
[default: 1000]
--force-mapper <FORCE_MAPPER>
Force a cartridge mapper, bypassing header auto-detection. Needed for headerless homebrew test ROMs (e.g. the `PeterLemon` Super FX / GSU plot tests). One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION>
Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--power-on <POWER_ON>
What RAM holds before the ROM boots: `zero` (default), `ones`, `random` (a seed is derived and printed) or `random=<seed>` (decimal or 0x hex — replays an exact machine). Fills WRAM, VRAM, CGRAM, OAM, APU RAM and the cartridge RAM no battery keeps (Game Pak RAM, BW-RAM, I-RAM); a boot bug that only shows on real hardware's garbage RAM shows here too
--dsp1-rom <DSP1_ROM>
Install a DSP coprocessor firmware (`dsp1b.rom`) into luna's firmware folder, then load — needed for DSP-1 games (Super Mario Kart, Pilotwings). Persists for future runs
--sym <SYM>
Load a WLA-DX `.sym` symbol file (overrides the automatic `<rom>.sym` detection). Symbol names then annotate the CPU disassembly and resolve in the debug tooling
--load-state <LOAD_STATE>
Load a save state (a `.luna` blob from the GUI's save-state slots, `~/.local/luna/states/<rom-slug>.slot<N>.luna`) right after the ROM loads, before the `-n` warm-up. Lets a headless run resume from a GUI-captured scene (e.g. to inspect a bug the CLI can't reach without input). The state must match this ROM
--wdm-out <WDM_OUT>
Write captured `WDM $xx` executions (the SDK breakpoint / `SNES_ASSERT` channel) to this file — parity with `luna run`, so an input-driven test keeps the WDM oracle
--print-fbhash
Print `fbhash=<16-hex>` — the cross-arch-stable displayed-frame key (same as `luna run --print-fbhash`) — so an input-driven `state` run can also emit a visual baseline
--call-stack
Track the 65C816 call stack (JSR/JSL/RTS/RTL + interrupts) during the run; the `--out` JSON gains a `call_stack` array of `{pc, from, kind, symbol}` frames
--native-res
Emit the native 512×448 frame: hi-res modes 5/6 and pseudo-512 keep their two horizontal subpixels per dot, interlace keeps both fields as separate lines — instead of the default 256×224 averaged view. Applies to `--screenshot` and `--print-fbhash`
--dump-vram <DUMP_VRAM>
Dump all 64 KB of PPU VRAM (raw bytes) to this file after the run. For diagnosing the framebuffer DMA → VRAM → display path
--dump-coproc-ram <DUMP_COPROC_RAM>
Dump the coprocessor work RAM (Super FX Game Pak RAM) raw bytes to this file, bypassing GSU ownership gating. For comparing luna's CPU-prepared GSU inputs against a reference
--dump-aram <DUMP_ARAM>
Dump all 64 KB of APU audio RAM (ARAM) raw bytes to this file. For diagnosing the SPC700 sound driver / CPU↔SPC handshake
--out <OUT>
Where to write the JSON state. Use `-` for stdout
[default: -]
--screenshot <SCREENSHOT>
Optional screenshot output path (PNG)
--audio-out <AUDIO_OUT>
Optional audio dump path (32 kHz stereo WAV)
--input <INPUT>
Scripted joypad-1 input. Format: comma-separated `frame:hex` checkpoints (frame number in decimal, hex mask with optional `0x` prefix). The mask is latched at the start of the named PPU frame and held until the next checkpoint overrides it.
Example: `--input "100:0x1000,110:0"` holds Start (`$1000`) for frames 100..=109 then releases.
JOY1 bit layout: B(15) Y(14) Sel(13) Start(12) Up/Down/Left/Right(11..8) A(7) X(6) L(5) R(4).
--input2 <INPUT2>
Scripted joypad-2 input, same `frame:hex` grammar as `--input` (a two-player probe, or replaying an MCP `script_p2` capture)
--input3 <INPUT3>
Scripted input for Super Multitap pads B, C, D — players 3, 4, 5 with `--port2 multitap` (pad A is player 2, `--input2`). Same grammar as `--input`
--input4 <INPUT4>
Player 4 (multitap pad C) — see `--input3`
--input5 <INPUT5>
Player 5 (multitap pad D) — see `--input3`
--port1 <PORT1>
Controller port-1 device: `pad` (default), `mouse`, `superscope`, `multitap`, or `none` for an unplugged port (its data lines read 0 for ever, where a pad's idle high — auto-read cannot tell them apart, only a manual read past bit 15 can)
[default: pad]
--port2 <PORT2>
Controller port-2 device: `pad` (default), `mouse`, `superscope`, `multitap` (players 2-5: `--input2` … `--input5`), or `none` for an unplugged port — see `--port1`
[default: pad]
--mouse <MOUSE>
Scripted SNES Mouse motion, applied to whichever port is set to `mouse`. `frame:dx,dy,buttons` entries separated by `;` (signed dx/dy; buttons bit0=left, bit1=right). Example: `--mouse "60:5,-3,1"`
--superscope <SUPERSCOPE>
Scripted Super Scope aim, applied to whichever port is set to `superscope`. `frame:x,y,buttons` entries separated by `;` (absolute screen pixels; buttons bit0=trigger, bit1=cursor, bit2=turbo, bit3=pause). Example: `--superscope "120:128,112,1"`
--peek <PEEK>
Optional memory peek(s) after snapshot. Format: `BANK:OFFSET:COUNT` (ALL hex — `:20` is 32 bytes; a count can say its base, `:0x20` hex or `:#32` decimal), or a WLA-DX label `NAME[:COUNT]` resolved through the loaded `.sym` table (auto-detected `<rom>.sym` or `--sym`; count defaults to 1). Can be specified multiple times. The whole 24-bit space is readable (WRAM, ROM incl. `$C0-$FF` `HiROM`, SRAM, coproc RAM); the `$2000-$5FFF` register band reads `0` (no side effects), except the DMA registers `$4300-$437F` which read their real values, and an unmapped range reads `$FF` with a stderr note and `unmapped` in the JSON entry. Output goes to stderr as a labelled hex dump. Examples: `--peek 7E:0200:220`, `--peek monster_x:2`. `APU:OFFSET:COUNT` reads the SPC700's RAM, and `VRAM:WORD:COUNT` reads `COUNT` bytes of PPU VRAM from a **word** address (as `VMADD` takes it): `--peek VRAM:7340:6` is three tilemap entries
--assert <ASSERT>
Assert memory equals expected bytes after warm-up. Format: `BANK:OFFSET=HEX` (all hex), or a WLA-DX label `NAME=HEX` resolved through the loaded `.sym` table. Prints `PASS`/`FAIL` per spec and exits non-zero if any fails. Repeatable. Examples: `--assert 7E:0010=AB12`, `--assert r_done=EFBE`
--assert-aram <ASSERT_ARAM>
Assert APU-RAM equals expected bytes (`OFFSET=HEX`). Like `--assert` but over ARAM. Repeatable
--assert-vram <ASSERT_VRAM>
Assert VRAM equals expected bytes (`OFFSET=HEX`). Like `--assert` but over VRAM. Repeatable
--assert-cgram <ASSERT_CGRAM>
Assert CGRAM (palette) equals expected bytes (`OFFSET=HEX`, byte offset into the 512-byte CGRAM, low byte of each colour first). Like `--assert` but over CGRAM. Repeatable
--until-frame <UNTIL_FRAME>
Run until PPU frame N (then snapshot), instead of stopping at the `-n` instruction count. Makes input→assert probes land on an exact frame rather than a generous `-n`
--until-pc <UNTIL_PC>
Run until execution reaches this routine (then snapshot): a `.sym` label, `label+N`, or `BANK:OFFSET` in hex. The stop is just before its first instruction runs, so `--peek` reads the state the routine receives — consistent where a frame boundary falls in the middle of a game tick. `--input` scripts stay indexed by frame. Bounded by `--until-frame` when given (else 36000 frames); not reaching it is an error, exit 1. The frame and scanline of the stop are printed and land in the JSON as `until_pc`. Example: `--until-pc tickEnd --hit 120`
--hit <HIT>
With `--until-pc`: stop the N-th time execution reaches it (default 1, the first)
[default: 1]
--peek-at <PEEK_AT>
Read every `--peek` EACH time execution reaches this routine (same forms as `--until-pc`), one line per arrival with its frame and scanline: a whole run's worth of consistent samples in one emulator run, with no trace code in the ROM. The run length stays `-n` / `--until-frame` / `--until-pc`. Rows go to stderr, to `--peek-at-out` as CSV, and into the JSON as `peek_hits`. Example: `--peek-at tickEnd --peek pl_x:#36 --until-frame 600`
--peek-at-out <PEEK_AT_OUT>
Write the `--peek-at` rows as CSV (`hit,frame,line` then one column of hex bytes per `--peek`) instead of printing them
--poke-at <POKE_AT>
Write every `--poke` into memory when execution reaches this routine (a `.sym` label, `label+N`, `BANK:OFFSET`), at its `--poke-hit`-th arrival, just before its first instruction: puts the game in a state it does not reach by itself, with no test code in the ROM. A `--peek-at` on the same routine reads the bytes written. Example: `--poke-at tickStart --poke-hit 91 --poke ball_x=4001 --poke ball_y=3000`
--poke-hit <POKE_HIT>
With `--poke-at`: the arrival the bytes are written at (default 1, the first)
[default: 1]
--poke <POKE>
Bytes to write at `--poke-at`: `NAME=HEX` or `BANK:OFFSET=HEX`, the `--assert` grammar — hex bytes in memory order, so a 16-bit `$0140` is `4001`. Repeatable
--input-at <INPUT_AT>
Index every input script (`--input` … `--input5`, `--mouse`, `--superscope`) by **arrivals on this routine** instead of by frame: entry `N:` applies the N-th time execution reaches it (a `.sym` label, `label+N`, or `BANK:OFFSET`). A frame number lands in a different game tick when the code gets faster or slower; an arrival does not, so the run depends on the game's logic alone. The press changes the controller, and the game sees it at its next read: with the auto-read, from arrival N+1. Example: `--input-at tickStart --input "81:0x0800,96:0"`
--srm-in <SRM_IN>
Load battery SRAM from a `.srm` file before running (the other half of a power-cycle test — write it with `--srm-out` in run A, read it back in run B)
--srm-out <SRM_OUT>
Write battery SRAM to a `.srm` file after the run
--apu-log <APU_LOG>
Optional CPU↔APU mailbox traffic log. When set, every CPU read/write of `$2140-$2143` during the run is captured and written to the given path as CSV with columns: `mclk_total,frame_ntsc,pc,kind,port,value` (one row per event). Useful for diagnosing APU handshake stalls (e.g. SMW's music-driver "wait for ack" deadlock)
--dsp-trace <DSP_TRACE>
Optional DSP register-write trace. Captures every write to a `$00-$7F` DSP register with an SPC-cycle timestamp and writes CSV columns `spc_cycles,reg,name,value` — `name` decodes the register (`V0_ADSR1`, `KON`, `FLG`, …). Answers "did my KON/KOFF sequence reach the chip in the order I intended?", which a WAV capture cannot show
--dsp-trace-max <DSP_TRACE_MAX>
Cap on captured DSP register writes (see `--dsp-trace`)
[default: 100000]
--sa1-log <SA1_LOG>
Optional SA-1 MMIO traffic log. When set, every CPU read/write of an SA-1 register `$2200-$23FF` during the run is captured and written to the given path as CSV with columns: `mclk_total,frame_ntsc,pc,kind,reg,value` (one row per event). Useful for diagnosing the CPU↔SA-1 handshake (e.g. the SMRPG intro deadlock)
--sa1-side-log <SA1_SIDE_LOG>
Optional SA-1-*side* execution log. When set, the SA-1's own reads/writes of its registers `$2200-$23FF` AND its *writes* to I-RAM (`$3000-$37FF`, reported as `$30xx` even via the `$0000-$07FF` mirror) are captured with the SA-1 PC and written as CSV (`seq,sa1_pc,kind,reg,value`). The I-RAM writes expose the cross-CPU handshake flags (e.g. Kirby's `$300A`/`$300E`) that the MMIO-only view can't show. Reads of I-RAM are NOT logged (they flood when the SA-1 spins on a flag). Complements `--sa1-log` (S-CPU side) to see why the SA-1 (re)asserts a register, e.g. the SMRPG SCNT=$87 loop
--sa1-trace <SA1_TRACE>
Optional FULL SA-1 instruction trace: a pre-opcode register snapshot per SA-1 instruction, written as CSV (`seq,pc,a,x,y,sp,p,db,dp,e`). Diff this PC stream against a bsnes/Mesen2 SA-1 trace to localise the SMRPG deadlock
--sa1-trace-max <SA1_TRACE_MAX>
Cap the SA-1 instruction trace at this many events (default 200 000)
[default: 200000]
--superfx-trace <SUPERFX_TRACE>
Optional FULL Super FX (GSU) instruction trace: a per-opcode snapshot written as CSV (`seq,mclk,go,stop,pc,opcode,sfr,r0..r15`). Diff this PC/register stream against a bsnes/siena GSU trace to localise rendering divergences
--superfx-trace-max <SUPERFX_TRACE_MAX>
Cap the Super FX instruction trace at this many events (default 200 000)
[default: 200000]
--superfx-trace-from <SUPERFX_TRACE_FROM>
Instruction count at which to begin the Super FX trace, like `--dma-trace-from` (an instruction count, not a frame: for frame N, take `stats.instructions_executed` from `luna state --until-frame N --out -`). The trace is a ring that keeps the most recent events once full, so the run's end picks the window; this trims its head, giving exactly [from, end] as long as that fits under `--superfx-trace-max`
[default: 0]
--gsu-bus-trace <GSU_BUS_TRACE>
Optional Super FX bus-ownership trace: every CPU read of Game Pak ROM or RAM made while the GSU owned it, as CSV (`seq,frame,line,mclk,pc,addr,kind`). Those reads return the busy vector or open bus — silently, on hardware too — so this names the instruction that did it. `gsu.bus_violations` in `--out -` counts them without the trace
--gsu-bus-trace-max <GSU_BUS_TRACE_MAX>
Cap the Super FX bus-ownership trace (default 200 000)
[default: 200000]
--dsp1-trace <DSP1_TRACE>
Optional DSP-1 (`µPD77C25`) trace: microcode execution AND the CPU-side DR/SR port traffic in ONE interleaved stream, as CSV (`seq,kind,pc,opcode,value,a,b,dr,sr,rqm`). `kind` is E/W/R/S (exec, DR write, DR read, SR poll) — so the command handshake is readable directly: did the byte land before or after RQM cleared? Parity with `--superfx-trace` / `--sa1-trace`
--dsp1-trace-max <DSP1_TRACE_MAX>
Cap the DSP-1 trace at this many events (default 200 000)
[default: 200000]
--dsp1-trace-ports
Restrict `--dsp1-trace` to the CPU-side DR/SR transactions, dropping microcode execution. The stock firmware idles in an RQM wait loop, so a full trace is almost entirely idle spin and the cap is exhausted before the interesting command lands — use this to read the command handshake itself
--dsp1-trace-commands <DSP1_TRACE_COMMANDS>
Group the DSP-1 port trace into command transactions, as CSV (`seq,cmd,name,pc,in_words,out_words,expected_in,expected_out, confidence,status,in,out`). One row per command: the byte, the input words it consumed, the output words it produced.
Transaction boundaries come from the protocol (an 8-bit `DRC` write opens a command), never from the word-count table — so a stale table entry surfaces as `status=mismatch` on that one row instead of silently mis-grouping the rest. `confidence` says how far the expected counts can be trusted; open-ended operations (Raster, ROM dump) report their observed length and assert nothing. Implies `--dsp1-trace-ports`.
--spc-trace <SPC_TRACE>
Optional FULL SPC700 instruction trace: a per-opcode register snapshot written as CSV (`seq,pc,a,x,y,sp,psw,spc_cycle,t2_int,t2_out`). Diff this PC stream against a Mesen2 SPC700 trace to localise audio-driver (Akao CPU↔SPC handshake) divergences (e.g. SMRPG/CT)
--spc-trace-max <SPC_TRACE_MAX>
Cap the SPC700 instruction trace at this many events (default 200 000)
[default: 200000]
--cpu-trace <CPU_TRACE>
Optional CPU instruction trace. When set, captures a per-instruction register snapshot (PC, A, X, Y, SP, P, DB, DP, e) into the given CSV file. Capture starts at instr count `--cpu-trace-from` (default 0) and stops after `--cpu-trace-max` events (default 100 000). Memory cost is ≈ 40 bytes × max-events
--cpu-trace-from <CPU_TRACE_FROM>
Instruction count at which to begin populating `--cpu-trace`. Default 0 (= capture from the very first step). Set to a value near the scene you want to debug to keep the buffer small
[default: 0]
--cpu-trace-max <CPU_TRACE_MAX>
Max number of trace events to capture (hard cap on log size). Default 100 000 ≈ 4 MB CSV
[default: 100000]
--mem-trace <MEM_TRACE>
Optional memory access trace. When set, captures every CPU bus read/write into a CSV at PATH, columns `mclk_total,frame_ntsc,pc,addr,kind,value,line,hclock,blank,force_blank,origin` (`line` / `hclock` = the beam position, `blank` = V-blank, `force_blank` = INIDISP `$2100` bit 7 at the access; a VRAM write is safe iff `blank||force_blank`; `origin` = `cpu`, `dma<n>` or `hdma<n>`). Default: all banks. Combine with `--mem-trace-bank 7E` to focus on WRAM and skip ROM fetches. Gated by `--mem-trace-from` and `--mem-trace-max` analogous to `--cpu-trace-*`
--mem-trace-from <MEM_TRACE_FROM>
[default: 0]
--mem-trace-max <MEM_TRACE_MAX>
[default: 100000]
--mem-trace-bank <MEM_TRACE_BANK>
Hex bank to filter the memory trace on (e.g. `7E` for WRAM main page). Omit to capture every access
--mem-trace-addr <MEM_TRACE_ADDR>
Hex offset range `LO:HI` (inclusive) to filter the memory trace on, e.g. `2100:21FF` to capture only PPU/CPU MMIO across all banks without the bank filter's code-fetch flood. Composes with `--mem-trace-bank` (both must match)
--trace-writes <TRACE_WRITES>
The "who wrote this register" hunt: record only WRITES to these hex offsets (any bank), e.g. `2121,2122,420C`. Each row's `origin` column says who — `cpu`, `dma<n>` or `hdma<n>` — with the frame / line / PC. The interrupt markers stay in the CSV to place the writes in time: `kind` `N` (NMI, at `$4210`) and `I` (IRQ, at `$4211`) — filter on `kind` `W` to count writes. Composes with `--mem-trace-bank`; needs `--mem-trace <PATH>`
--dma-trace <DMA_TRACE>
Optional DMA→VRAM transfer-time trace. Captures every byte an MDMA writes to `$2118/$2119` as CSV (`seq,frame,line,blank,force_blank,src,vram_word,reg,value`) — `blank` = V-blank period, `force_blank` = INIDISP (`$2100`) bit 7 at the write (a write is safe iff `blank||force_blank`), `src` the 24-bit A-bus source, `vram_word` the VMADD word the byte lands at, `reg` $18/$19. The byte is captured AS READ during the transfer, so a coprocessor (Super FX) overwriting its source buffer afterwards can't confound the source→VRAM comparison. Gated by `--dma-trace-from`/`--dma-trace-max`
--dma-trace-from <DMA_TRACE_FROM>
Instruction count at which to begin the DMA→VRAM trace
[default: 0]
--dma-trace-max <DMA_TRACE_MAX>
Max DMA→VRAM trace events (default 500 000)
[default: 500000]
-h, --help
Print help (see a summary with '-h')

luna frames

Capture a sequence of EXACTLY-consecutive PPU frames as PNGs in one run, via the same `luna-api` render path the GUI uses. Use this to diagnose *temporal* artefacts (flicker / double-buffer page-flip desync) that a single `state --screenshot` cannot show — it samples one frame, so a frame-to-frame "blink" is invisible to it. Each frame's PNG is tagged with its frame number and the INIDISP forced-blank flag, so you can see exactly what the GUI would (and would not) display
Usage: luna frames [OPTIONS] <ROM>
Arguments:
<ROM> Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS> Warm-up CPU instructions to execute before capturing begins [default: 1000]
--from-frame <FROM_FRAME> Start the capture at PPU frame N instead of after the `-n` warm-up instructions (which are then ignored) — the first PNG is frame N. Frame-indexed like `state --until-frame`, so the sequence is stable across codegen changes
-c, --count <COUNT> Number of consecutive frames to capture [default: 8]
--out-dir <OUT_DIR> Output directory for the PNG sequence (created if absent) [default: /tmp/luna_frames]
--force-mapper <FORCE_MAPPER> Force a cartridge mapper, bypassing header auto-detection. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION> Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--power-on <POWER_ON> What RAM holds before the ROM boots: `zero` (default), `ones`, `random` (a seed is derived and printed) or `random=<seed>` (decimal or 0x hex — replays an exact machine). Fills WRAM, VRAM, CGRAM, OAM, APU RAM and the cartridge RAM no battery keeps (Game Pak RAM, BW-RAM, I-RAM); a boot bug that only shows on real hardware's garbage RAM shows here too
--input <INPUT> Scripted joypad-1 input, same `frame:hex` format as `state --input`, applied during the warm-up so the capture can land in gameplay rather than at a title screen
-h, --help Print help

luna diff

Compare two ROMs frame by frame: run both in one process, hash the displayed frame at every PPU frame, and print MATCH / DIFF for each requested frame — MATCH when A's frame `F` equals B's frame at some `F ± tolerance` (the boot-length offset a codegen change can introduce). The "compare at equal frame" protocol that validates a compiler / library change. Exit 0 = all match, 1 = any DIFF, 2 = usage error.
With `--audio`, the two ROMs' sound is compared instead: both run to `--until-frame`, the output is cut into windows and each window's RMS level compared. MATCH when every window is within `--tolerance-pct` — the comparison that survives the same sound arriving a few samples earlier or later, which an audio hash does not.
With `--sequence`, time is ignored: every frame of `--from`..`--to` is hashed, each run of identical frames counts as one picture, and the longest run of pictures both ROMs show in the same order is measured. SAME-SEQUENCE when it reaches `--min-common` — the same animation at another cadence, or after a boot of another length.
Usage: luna diff [OPTIONS] <ROM_A> <ROM_B>
Arguments:
<ROM_A>
The reference build
<ROM_B>
The build under test
Options:
--frames <FRAMES>
PPU frames to compare, comma-separated (e.g. `200,400`)
--tolerance <TOLERANCE>
Accept a match up to this many frames away (`b = a ± n`)
[default: 0]
--sequence
Compare the sequence of pictures over a range of frames instead of the frames one by one
--from <FROM>
`--sequence`: first PPU frame of the range [default: 1]
--to <TO>
`--sequence`: last PPU frame of the range
--min-common <MIN_COMMON>
`--sequence`: SAME-SEQUENCE when the longest common run holds at least this many pictures
--min-common-pct <MIN_COMMON_PCT>
`--sequence`: SAME-SEQUENCE when the longest common run holds at least this percentage of the pictures of the ROM that shows fewer of them [default: 90]
--audio
Compare the audio output instead of the displayed frames
--until-frame <UNTIL_FRAME>
`--audio`: run both ROMs to this PPU frame
--window-ms <WINDOW_MS>
`--audio`: window length, in milliseconds of output [default: 500]
--tolerance-pct <TOLERANCE_PCT>
`--audio`: largest RMS difference a window may show, in percent of the louder of the two [default: 2]
--align-onset
`--audio`: compare each window with the stretch of the other ROM's output that fits it best within `--max-shift` samples, and print the shift kept — for the same sounds, each a few samples earlier or later
--max-shift <MAX_SHIFT>
`--align-onset`: how far a window may be moved, in samples. A frame is 534 [default: 64]
--silence <SILENCE>
`--audio`: sample level counted as silence (0-32767). It is the threshold of the reported onset, and the floor a window's difference is measured against [default: 64]
--input <INPUT>
Scripted joypad-1 input applied to BOTH machines (`state --input` grammar)
--screenshot-dir <SCREENSHOT_DIR>
Write `frame_<F>_a.png` / `frame_<F>_b.png` here for every DIFF frame
--out <OUT>
Also write a JSON report (`-` = stdout after the text lines)
--force-display
Hash the frame with INIDISP forced-blank bypassed (as `run --force-display`)
--native-res
Hash the native 512×448 frame
--force-mapper <FORCE_MAPPER>
Force a cartridge mapper for both ROMs. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION>
Force the video standard for both ROMs (ntsc, pal)
--power-on <POWER_ON>
Power-on RAM state for both machines (`zero`, `ones`, `random[=<seed>]`)
-h, --help
Print help (see a summary with '-h')

luna profile

Real master cycles per symbol: every step credits its cycles — bus + internal cycles plus the DMA / HDMA / refresh stalls charged during it — to the instruction's address; the report folds those onto the nearest `.sym` label (or a 256-byte page without one), heaviest first. Replaces static instruction weights with what the machine paid
Usage: luna profile [OPTIONS] <ROM>
Arguments:
<ROM> Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS>
CPU instructions to profile (after `--from-frame`) [default: 3000000]
--until-frame <UNTIL_FRAME>
Profile until PPU frame N instead of the `-n` count
--from-frame <FROM_FRAME>
Start profiling at PPU frame N (warm-up before it is not counted) — e.g. skip the boot to profile the game loop [default: 0]
--input <INPUT>
Scripted joypad-1 input (`state --input` grammar)
--input2 <INPUT2>
Scripted joypad-2 input (`state --input2` grammar)
--input3 <INPUT3>
Multitap pads B, C, D — players 3, 4, 5 (`state --input3`)
--input4 <INPUT4>
Player 4 (multitap pad C) — see `--input3`
--input5 <INPUT5>
Player 5 (multitap pad D) — see `--input3`
--port1 <PORT1>
Controller port-1 device: `pad` (default), `mouse`, `superscope`, `multitap`, or `none` for an unplugged port (its data lines read 0 for ever, where a pad's idle high — auto-read cannot tell them apart, only a manual read past bit 15 can) [default: pad]
--port2 <PORT2>
Controller port-2 device: `pad` (default), `mouse`, `superscope`, `multitap`, or `none` for an unplugged port — see `--port1` [default: pad]
--mouse <MOUSE>
Scripted SNES Mouse motion (`state --mouse` grammar), applied to whichever port is set to `mouse`
--superscope <SUPERSCOPE>
Scripted Super Scope aim (`state --superscope` grammar), applied to whichever port is set to `superscope`
--sym <SYM>
WLA-DX `.sym` to fold PCs onto (overrides the `<rom>.sym` auto-detection)
--top <TOP>
Rows to print (the JSON report always has them all) [default: 25]
--out <OUT>
Write the full report as JSON (`-` = stdout after the table)
--pc-set <PC_SET>
Write the set of executed 24-bit PCs (sorted, deduplicated, little-endian `u32` each) to this file — the input a code-coverage tool folds onto symbols and source lines. Whole 24-bit space: `HiROM` `$C0-$FF`, `FastROM` mirrors and WRAM-resident code included
--budget <BUDGET>
Gate: `SYMBOL=MCLK` — the symbol's worst completed frame must not exceed MCLK master cycles, else exit 1 (repeatable; an unknown symbol is a usage error, exit 2). The VBlank-budget check for CI: `--budget NmiHandler=6000`
--stack-floor <STACK_FLOOR>
Gate: the stack must never reach below this address, else exit 1 — the RAM-budget check a link-time guess cannot make (`--stack-floor 0x1C60`). Accepts `0x`- or `$`-hex, or decimal. The figure is reported either way, with the routine that went deepest when a `.sym` is loaded
--gsu-pc-set <GSU_PC_SET>
Write the set of executed **GSU** PCs (same encoding as `--pc-set`): a separate file, because a GSU PC and a 65816 PC can be the same number and mean different code — merging them would mis-attribute coverage. Lets a coverage tool count `.sfx`
--input-at <INPUT_AT>
Index the input scripts by arrivals on this routine instead of by frame (`state --input-at`): the same script then drives two builds of a game through the same ticks, whatever their speed
--until-pc <UNTIL_PC>
End the profile when execution reaches this routine (a `.sym` label, `label+N`, or `BANK:OFFSET` in hex), just before its first instruction: a window of N game ticks rather than N frames, which stays the same work when the code gets faster. Bounded by `--until-frame` when given (else 36000 frames; `-n` does not apply); not reaching it is an error, exit 1. Example: `--from-frame 60 --until-pc tickEnd --hit 340`
--hit <HIT>
With `--until-pc`: which arrival, counted from power-on [default: 1]
--frames-out <FRAMES_OUT>
Write the window frame by frame as CSV (`frame,active_mclk,idle_mclk,cpu_mclk,dma_mclk,hdma_mclk,refresh_mclk,total_mclk,nmi,lag`): `active` is what the program used (CPU + the DMA it started), `idle` the time parked in `WAI`, `lag` a frame whose NMI found the CPU still executing. The same rows are `frame_series` in `--out`. Finds WHICH frame overran, in one run
--worst <WORST>
Print the per-symbol table of the N heaviest frames (by `active_mclk`), and put them in `--out` as `worst_frames` — what was running in the frame that overran [default: 0]
--max-frame-mclk <MAX_FRAME_MCLK>
Gate: no completed frame may use more than N active master cycles, else exit 1
--max-lag-frames <MAX_LAG_FRAMES>
Gate: at most N lag frames in the window, else exit 1. A lag frame is one whose NMI came while the CPU was executing rather than parked in `WAI`
--max-lag-run <MAX_LAG_RUN>
Gate: at most N lag frames IN A ROW, else exit 1. A game whose tick takes two frames lags every other frame by design: `--max-lag-run 1` passes it and fails the tick that spills into a third
--force-mapper <FORCE_MAPPER>
Force a cartridge mapper. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION>
Force the video standard (ntsc, pal)
--power-on <POWER_ON>
Power-on RAM state (`zero`, `ones`, `random[=<seed>]`)
-h, --help
Print help

luna wram-trace

Emit per-frame (vblank-aligned) WRAM page hashes for a confound-free cross-emulator differential. Each line is `<ppu_frame> <h0> <h1> ... <hN>` where each `h` is the FNV-1a hash of one WRAM page (`--page-size` bytes, default 4 KiB → 32 pages). Because WRAM-at-vblank-N is the SAME game-frame in both luna and a reference emulator (no input ⟹ game logic advances once per NMI), the first frame whose page hash differs from the reference pins the first REAL state divergence — unlike scene- level windows the boot-frame offset confounds
Usage: luna wram-trace [OPTIONS] <ROM>
Arguments:
<ROM> Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS> Warm-up CPU instructions before frame-0 of the trace [default: 0]
-c, --count <COUNT> Number of consecutive frames to hash [default: 300]
--page-size <PAGE_SIZE> WRAM page size in bytes (power of two dividing 0x20000) [default: 4096]
--out <OUT> Output path for the hash table (one line per frame) [default: /tmp/luna_wram_hashes.txt]
--dump-frame <DUMP_FRAME> Optionally also dump the full 128 KiB WRAM as a raw .bin when the trace reaches this PPU frame (for byte-level diffing)
--dump-out <DUMP_OUT> Output path for the `--dump-frame` raw WRAM snapshot [default: /tmp/luna_wram_frame.bin]
--force-mapper <FORCE_MAPPER> Force a cartridge mapper. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION> Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--input <INPUT> Scripted joypad-1 input, same `frame:hex` format as `state --input`
-h, --help Print help

luna bench

Run every ROM in a directory headless, detect anomalies (crashes, freezes, dead APU, missing firmware), and write a compatibility report + one markdown bug file per finding. Stresses the CLI/API across the whole corpus. Reports stay local (under `--out`)
Usage: luna bench [OPTIONS] [DIR]
Arguments:
[DIR] Directory of ROMs to scan (`.sfc` / `.smc`) [default: tests/roms]
Options:
--out <OUT> Output directory for the report, screenshots, and bug files [default: tests/roms/bench]
-f, --frames <FRAMES> Frames to run per ROM [default: 600]
--input <INPUT> Override the default Start-pulse input (`frame:hex`, like `state --input`) applied to clear title screens
-h, --help Print help

luna spc-dump

Run a ROM until its music driver is playing, then export the APU state as a `.spc` sound file (SNES-SPC700 Sound File Data v0.30): SPC700 registers + 64 KB ARAM + DSP registers + IPL ROM, playable in any SPC player. Step far enough in (and pulse Start via `--input`) that the music has started before the snapshot
Usage: luna spc-dump [OPTIONS] <ROM>
Arguments:
<ROM> Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS> CPU instructions to execute before the snapshot [default: 5000000]
-o, --out <OUT> Output path for the `.spc`. Defaults to the ROM's name with a `.spc` extension, in the current directory
--force-mapper <FORCE_MAPPER> Force a cartridge mapper. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION> Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--dsp1-rom <DSP1_ROM> Install a DSP coprocessor firmware (`dsp1b.rom`) then load — needed for DSP-1 games (Super Mario Kart, Pilotwings)
--input <INPUT> Scripted joypad-1 input, same `frame:hex` format as `state --input`, applied before the snapshot so playback can start past a title screen
-h, --help Print help

luna assets-dump

Run a ROM to a scene, then dump every graphics asset currently loaded as PNGs: the VRAM tile sheet, the four BG tilemaps, the CGRAM palette, the OAM sprite sheet, and the composited screen — plus raw `vram.bin` / `cgram.bin` and `oam.json`. This captures the assets *loaded at this instant* (already decompressed by the game); snapshot several scenes (via `--input` / different `-n`) to cover a whole game. A static whole-ROM rip is NOT possible — SNES graphics are game-specific-compressed with no standard layout
Usage: luna assets-dump [OPTIONS] <ROM>
Arguments:
<ROM> Path to the .sfc / .smc ROM file
Options:
-n, --steps <STEPS> CPU instructions to execute before the snapshot [default: 5000000]
--until-frame <UNTIL_FRAME> Run until PPU frame N (then dump), instead of stopping at the `-n` instruction count — same as `state --until-frame`. Takes the assets at the frame a manifest checks, whatever the speed of the code that got there
--out <OUT> Output directory (created if absent) [default: /tmp/luna_assets]
--bpp <BPP> VRAM tile-sheet bits-per-pixel (2/4/8). Default: auto from the current BG1 mode
--palette <PALETTE> CGRAM sub-palette row for the VRAM tile sheet (2bpp/4bpp) [default: 0]
--force-mapper <FORCE_MAPPER> Force a cartridge mapper. One of: lorom, hirom, exhirom, sa1, superfx, dsp1, sdd1; spc7110 is recognised but not emulated, the load refuses it
--force-region <FORCE_REGION> Force the video standard (ntsc, pal), overriding the cartridge header's country byte. Changes the scanline count (262/312) and frame rate — required to reproduce the golden harness, which runs the `PeterLemon` corpus as PAL to match krom's reference captures
--dsp1-rom <DSP1_ROM> Install a DSP coprocessor firmware (`dsp1b.rom`) then load
--input <INPUT> Scripted joypad-1 input, same `frame:hex` format as `state --input`, applied before the snapshot
-h, --help Print help