jonathanpeppers/dotnes/nes-emu-debug

Claude Code and agent skill by jonathanpeppers/dotnes

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Run NES ROMs in the MesenCE emulator to debug runtime behavior. Use this skill whenever the user wants to run a ROM and inspect what actually happens at runtime: read NES memory (RAM, palette, nametable, OAM), dump CPU/PPU/APU state after N frames, capture the screen buffer, compare runtime behavior between two ROMs, or verify that a sample displays correctly. Also use when the user says things like "run the ROM", "what does the screen look like", "check the palette", "inspect nametable", "read zero page", "dump memory", or "the ROM doesn't display correctly". This is the dynamic/runtime counterpart to nes-rom-debug (which does static binary analysis).

Install

npx vskill@latest install jonathanpeppers/dotnes/nes-emu-debug

SKILL.md

NES Emulator Debug (MesenCE)

Run NES ROMs headlessly in MesenCE's test runner to inspect runtime state, memory, and screen output. This complements the nes-rom-debug skill (static binary analysis) with dynamic runtime inspection.

Prerequisites

Build the local packages, then a sample to install MesenCE:

powershell
dotnet build
dotnet build samples\hello\hello.csproj

The dotnes.mesen 2.2.1 package downloads MesenCE from nesdev-org/MesenCE into its NuGet package's bin directory. Building the packaging project alone does not download the emulator. Query the sample's evaluated RunCommand rather than hardcoding the cache path or platform-specific executable.

How It Works

MesenCE has a headless test runner mode that loads a ROM, runs a Lua script, and exits — no window, no GUI. The Lua script has full access to the emulator API: CPU/PPU/APU state, all memory types, screen buffer, frame callbacks, etc.

Basic Invocation
powershell
# Find the Mesen executable (works on any OS / config)
$mesenExe = dotnet msbuild samples\hello\hello.csproj -getProperty:RunCommand
$mesen = (Resolve-Path $mesenExe).Path
$rom = (Resolve-Path "path/to/rom.nes").Path
$script = (Resolve-Path "path/to/script.lua").Path
$proc = Start-Process -FilePath $mesen -ArgumentList `
"--testRunner", # Headless mode (no window)
"--enableStdout", # Show ROM loading info on stdout
"--doNotSaveSettings", # Don't persist config changes
"--timeout=30", # Kill after N seconds if script doesn't call emu.stop()
"--debug.scriptWindow.allowIoOsAccess=true", # Enable Lua file I/O
$script, $rom `
-PassThru -RedirectStandardOutput out.txt -RedirectStandardError err.txt -NoNewWindow
$proc.WaitForExit(35000)
# Exit code comes from emu.stop(N) in the Lua script, or -1 on timeout
Key Command-Line Flags
FlagPurpose
--testRunnerHeadless mode — no window, runs at max speed, exits via emu.stop(code)
--enableStdoutPrint emulator log (ROM info, mapper details) to stdout
--doNotSaveSettingsDon't write settings.json (safe for automation)
--timeout=NKill if script doesn't exit within N seconds (default: 100)
--debug.scriptWindow.allowIoOsAccess=trueRequired for Lua io.open/os.* functions

Lua Script API Reference

State Access

The state table returned by emu.getState() uses flat dotted-string keys, not nested tables. Access fields with bracket notation:

lua
local state = emu.getState()
-- CORRECT:
local pc = state["cpu.pc"]
local scanline = state["ppu.scanline"]
-- WRONG (nil!):
-- local pc = state.cpu.pc

Key state fields:

KeyTypeDescription
cpu.pcnumberProgram counter
cpu.anumberAccumulator
cpu.xnumberX index register
cpu.ynumberY index register
cpu.spnumberStack pointer
cpu.psnumberProcessor status flags
cpu.cycleCountnumberTotal CPU cycles elapsed
ppu.scanlinenumberCurrent PPU scanline
ppu.cyclenumberCurrent PPU cycle within scanline
ppu.frameCountnumberTotal PPU frames rendered
ppu.control.*variousPPU control register bits
ppu.mask.*variousPPU mask register bits
ppu.statusFlags.*variousPPU status flags
frameCountnumberEmulator frame count
masterClocknumberMaster clock ticks
Memory Types

Use emu.read(address, memType) to read memory. The address is relative to the start of that memory region (not the CPU/PPU mapped address).

Memory TypeEnum ValueSizeDescription
emu.memType.nesInternalRam462 KBCPU RAM ($0000-$07FF). Use this for zero page.
emu.memType.nesPaletteRam5332 BBackground + sprite palette (addr 0-31)
emu.memType.nesNametableRam492 KBVRAM nametables (addr 0 = NT0 tile 0,0)
emu.memType.nesSpriteRam51256 BOAM — 64 sprites × 4 bytes each
emu.memType.nesPrgRom45variesPRG ROM (addr 0 = first PRG byte)
emu.memType.nesChrRom55variesCHR ROM pattern tables
emu.memType.nesChrRam54variesCHR RAM (if mapper uses RAM instead of ROM)
emu.memType.nesWorkRam47variesBattery-backed / work RAM
emu.memType.nesSaveRam48variesSave RAM
emu.memType.nesMemory864 KBFull CPU address space (mapped, may cause side effects)
emu.memType.nesPpuMemory916 KBFull PPU address space (mapped)

Important: For reading zero page / RAM, use nesInternalRam (direct access), not nesMemory (which goes through the CPU bus and may return stale/zero values in test runner mode). Similarly, use nesPaletteRam for palette, not nesPpuMemory.

Frame Callbacks

Register a callback to run after each frame:

lua
local frameCount = 0
emu.addEventCallback(function()
frameCount = frameCount + 1
if frameCount == 120 then
-- Do your inspection here
emu.stop(0) -- exit with code 0
end
end, emu.eventType.endFrame)
Screen Buffer
lua
local pixels = emu.getScreenBuffer()
-- Returns a Lua table of 61440 ARGB integer values (256 × 240 NES resolution)
-- Each pixel: (A << 24) | (R << 16) | (G << 8) | B

To save as raw RGB file:

lua
local sf = io.open("screen.raw", "wb")
for i = 1, #pixels do
local p = pixels[i]
sf:write(string.char((p >> 16) & 0xFF, (p >> 8) & 0xFF, p & 0xFF))
end
sf:close()
-- Result: 184320 bytes (256 × 240 × 3 RGB), convertible with ImageMagick/ffmpeg

Convert to PNG (from PowerShell):

powershell
# Using Python PIL/Pillow
python -c "
from PIL import Image
raw = open('screen.raw','rb').read()
img = Image.frombytes('RGB', (256,240), raw)
img.save('screenshot.png')
"
# Or using ffmpeg
ffmpeg -f rawvideo -pixel_format rgb24 -video_size 256x240 -i screen.raw screenshot.png
Exiting
lua
emu.stop(0) -- exit test runner with code 0 (success)
emu.stop(1) -- exit with code 1 (failure)
emu.stop(42) -- any integer exit code

The exit code is returned as the process exit code, so you can check it from PowerShell via $proc.ExitCode.

Lua Script Templates

Template 1: Dump CPU State After N Frames
lua
-- dump_state.lua — Dump CPU/PPU state after N frames
-- Usage: Mesen.exe --testRunner ... dump_state.lua rom.nes
local FRAMES = 120 -- wait 2 seconds (60 fps)
local OUT_FILE = "OUTPUT_PATH" -- replace with absolute path using forward slashes
local frameCount = 0
emu.addEventCallback(function()
frameCount = frameCount + 1
if frameCount == FRAMES then
local state = emu.getState()
local f = io.open(OUT_FILE, "w")
f:write("=== CPU State ===\n")
f:write(string.format("PC=$%04X A=$%02X X=$%02X Y=$%02X SP=$%02X PS=$%02X\n",
state["cpu.pc"], state["cpu.a"], state["cpu.x"],
state["cpu.y"], state["cpu.sp"], state["cpu.ps"]))
f:write("Cycles: " .. state["cpu.cycleCount"] .. "\n")
f:write("\n=== PPU State ===\n")
f:write("Scanline: " .. state["ppu.scanline"] .. " Cycle: " .. state["ppu.cycle"] .. "\n")
f:write("Frame: " .. state["ppu.frameCount"] .. "\n")
f:write("NMI enabled: " .. tostring(state["ppu.control.nmiOnVerticalBlank"]) .. "\n")
f:write("BG enabled: " .. tostring(state["ppu.mask.backgroundEnabled"]) .. "\n")
f:write("Sprites enabled: " .. tostring(state["ppu.mask.spritesEnabled"]) .. "\n")
f:close()
emu.stop(0)
end
end, emu.eventType.endFrame)
Template 2: Read NES Library Zero Page Variables

The NES library stores state in zero page. Key addresses (from src/dotnes.tasks/Utilities/NESConstants.cs):

AddressNameDescription
$01STARTUPStartup flag
$02NES_PRG_BANKSNumber of PRG banks
$03VRAM_UPDATENon-zero = VRAM update pending
$04-$05NAME_UPD_ADRNametable update address (16-bit)
$06NAME_UPD_ENABLENametable update enable flag
$07PAL_UPDATEPalette update pending
$08-$09PAL_BG_PTRBackground palette pointer (16-bit)
$0A-$0BPAL_SPR_PTRSprite palette pointer (16-bit)
$0CSCROLL_XHorizontal scroll position
$0DSCROLL_YVertical scroll position
$0ESCROLL_X1split() saved X scroll
$0FPPU_CTRL_VAR1split() saved PPU_CTRL
$10PRG_FILEOFFSPRG file offset
$12PPU_MASK_VARShadow of PPU mask register
$14-$16NMI_CALLBACKJMP opcode + address for NMI callback
$17TEMPTemporary variable
$18TEMP_HITemp high byte / DUP_TEMP
$19TEMP2Additional temp
$1ATEMP3Additional temp
$1BOAM_OFFOAM buffer offset
$1CUPDPTRVRAM update buffer index
$22spC stack pointer
$3CRAND_SEEDRandom seed for PRNG
lua
-- read_zeropage.lua
local f = io.open("OUTPUT_PATH", "w")
local frameCount = 0
emu.addEventCallback(function()
frameCount = frameCount + 1
if frameCount == 120 then
f:write("=== NES Library Zero Page ===\n")
-- Names from src/dotnes.tasks/Utilities/NESConstants.cs
local names = {
[0x01]="STARTUP", [0x02]="NES_PRG_BANKS", [0x03]="VRAM_UPDATE",
[0x04]="NAME_UPD_ADR", [0x06]="NAME_UPD_ENABLE", [0x07]="PAL_UPDATE",
[0x08]="PAL_BG_PTR", [0x0A]="PAL_SPR_PTR",
[0x0C]="SCROLL_X", [0x0D]="SCROLL_Y", [0x0E]="SCROLL_X1",
[0x0F]="PPU_CTRL_VAR1", [0x10]="PRG_FILEOFFS", [0x12]="PPU_MASK_VAR",
[0x14]="NMI_CALLBACK", [0x17]="TEMP", [0x18]="TEMP_HI",
[0x19]="TEMP2", [0x1A]="TEMP3", [0x1B]="OAM_OFF", [0x1C]="UPDPTR",
[0x22]="sp", [0x3C]="RAND_SEED"
}
for addr = 0, 31 do
local val = emu.read(addr, emu.memType.nesInternalRam)
local name = names[addr] or ""
f:write(string.format(" $%02X = $%02X %s\n", addr, val, name))
end
f:close()
emu.stop(0)
end
end, emu.eventType.endFrame)
Template 3: Dump Palette
lua
-- dump_palette.lua
local f = io.open("OUTPUT_PATH", "w")
local frameCount = 0
emu.addEventCallback(function()
frameCount = frameCount + 1
if frameCount == 120 then
f:write("=== Palette RAM ===\n")
f:write("Background palettes:\n")
for pal = 0, 3 do
f:write(string.format(" Palette %d: ", pal))
for i = 0, 3 do
local val = emu.read(pal * 4 + i, emu.memType.nesPaletteRam)
f:write(string.format("$%02X ", val))
end
f:write("\n")
end
f:write("Sprite palettes:\n")
for pal = 0, 3 do
f:write(string.format(" Palette %d: ", pal + 4))
for i = 0, 3 do
local val = emu.read(16 + pal * 4 + i, emu.memType.nesPaletteRam)
f:write(string.format("$%02X ", val))
end
f:write("\n")
end
f:close()
emu.stop(0)
end
end, emu.eventType.endFrame)
Template 4: Dump Nametable Region
lua
-- dump_nametable.lua — Read a rectangular region of the nametable
local NT_BASE = 0 -- 0 for nametable 0, 0x400 for nametable 1
local START_ROW = 0
local END_ROW = 29 -- 30 rows total
local START_COL = 0
local END_COL = 31 -- 32 columns total
local f = io.open("OUTPUT_PATH", "w")
local frameCount = 0
emu.addEventCallback(function()
frameCount = frameCount + 1
if frameCount == 120 then
f:write("=== Nametable ===\n")
for row = START_ROW, END_ROW do
f:write(string.format("Row %2d: ", row))
for col = START_COL, END_COL do
local addr = NT_BASE + row * 32 + col
local tile = emu.read(addr, emu.memType.nesNametableRam)
if tile == 0 then
f:write(".. ")
else
f:write(string.format("%02X ", tile))
end
end
f:write("\n")
end
f:close()
emu.stop(0)
end
end, emu.eventType.endFrame)
Template 5: Capture Screenshot as Raw RGB
lua
-- capture_screen.lua
local FRAMES = 120
local RAW_FILE = "OUTPUT_PATH.raw"

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
673
Repository forks
24
7-day trend score
33

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/jonathanpeppers/dotnes/nes-emu-debug/badge)](https://verified-skill.com/skills/jonathanpeppers/dotnes/nes-emu-debug)

Something wrong with this skill or its page? Tell us.