Skip to content

Debugging chippy programs from your editor

chippy ships with a Debug Adapter Protocol server so any DAP-aware editor can drive the emulator. The TUI is still the default; DAP mode is a separate process you start with the -dap flag.

Launching the adapter

Four transports:

# stdio — editor spawns the binary and pipes stdin/stdout.
# Used by VS Code's default DebugAdapterExecutable.
chippy -dap stdio

# TCP — adapter listens, editor connects out.
# Used by nvim-dap's default `executable` block.
chippy -dap tcp:14785

# unix — lowest-overhead out-of-process local transport
# (nvim-dap / vscode-chippy on the same host).
chippy -dap unix:/tmp/chippy.sock

# inproc — in-process loopback self-check; the transport itself
# (dap.NewInprocServer) is the foundation for the embedded TUI-via-DAP build.
chippy -dap inproc

-dap is mutually exclusive with the TUI: when set, chippy never opens the alt-screen and instead speaks JSON-RPC over the chosen channel.

Transport overhead

A stepIn round-trip on the same host (NOP sled, go test -bench, Apple M-series):

Transport Round-trip Notes
inproc ~0.34 µs dap.NewInprocServer — Request/Response structs pass straight to the dispatcher; nil-args requests and all responses make the trip with zero serialization.
unix ~30 µs full JSON wire framing over a unix-domain socket.

The wire transports (stdio/tcp/unix) are interchangeable — they marshal the same JSON-RPC. inproc trades wire compatibility for an ~90× faster local round-trip, which the long-term TUI-via-DAP architecture (issue #394) needs.

VS Code

Copy examples/dap/launch.json into your project's .vscode/launch.json and adjust paths. The launch config maps 1:1 to the CLI flags:

{
  "type": "chippy",
  "request": "launch",
  "name": "Run chippy",
  "rom": "${workspaceFolder}/build/program.bin",
  "cpuVariant": "65c02",
  "dbgPath": "${workspaceFolder}/build/program.dbg",
  "stopOnEntry": true
}

stopOnEntry is honored:

  • absent (default): pause at the reset vector and emit a stopped(entry) event
  • true: same as absent
  • false: skip the entry pause and auto-start the run loop after launch

Same field on attach skips/emits the entry stopped event without spawning a new run goroutine — the host process drives execution.

TUI attach (:dap)

The TUI can spawn its own DAP listener so an editor can attach to the same running session:

:dap 14785       — start TCP listener on :14785
:dap 0           — auto-assign a free port
:dap             — report current listener
:dap stop        — close it

Once the listener is up, point your editor at localhost:<port> with an attach launch config (chippy's attach request returns stopped(entry) and exposes the running CPU). Both the TUI and the editor drive the same CPU — chippy serializes access via a shared mutex so steps from either side never race.

Only one listener is live at a time. A second :dap PORT reports "already listening".

nvim-dap

Copy examples/dap/nvim-dap.lua into your config. It registers the chippy adapter (TCP transport on 14785) and a default configuration that launches a program from cwd/build/program.bin.

require("dap").adapters.chippy = {
  type = "server",
  port = 14785,
  executable = { command = "chippy", args = { "-dap", "tcp:14785" } },
}

<F5> to launch, <F10> step over, <F11> step into, <Shift-F11> step out.

Supported requests

Request Issue Notes
initialize #47 Negotiates capabilities; reports the full subset below in one shot.
launch #47 Takes rom, loadAddr, resetVec, linkerCfg, dbgPath, cpuVariant, tracePath, stopOnEntry.
attach #87 Supported when the host process pre-populates the debuggee via Server.AttachExisting. The in-TUI listener that ties this to the :dap command is filed as a follow-up (#97).
disconnect / terminate #47 Tears down the run goroutine, closes the trace file, exits.
continue / pause #50 Continue spawns a CPU run goroutine; pause flips a signal.
next / stepIn / stepOut #50 Step-over runs to PC+3 past a JSR; step-out runs until SP rises.
stepBack #79 Pops one snapshot from the 256-entry rewind ring and restores CPU+RAM. Only pre-step paths push — continue runs aren't reversible.
threads #50 One virtual thread (id=1, name=cpu).
stackTrace #48, #449 Walks JSR frames via cpu.DetectStackFrame. Each frame adds two chippy-extension fields (additive; standard clients ignore them): chippyStackAddr ($01XX stack-page slot of the pushed return pair) and chippyCallee (symbol at the JSR target). The chippy TUI's stack panel renders from these (#449).
scopes #48, #410 Registers, Flags, and (when a .dbg is loaded) Globals.
variables #48, #410 ref=1 → A/X/Y/SP/PC/P/Cycles; ref=2 → 8 P-flag bits; ref=3 → data symbols (sized .dbg globals + data-range symbols, code labels filtered). A symbol with size>1 expands into indexed byte children via a dynamically-allocated variablesReference (paged with start/count; supportsVariablePaging).
setVariable #48, #454 Writes hex / decimal to a register, flag bit, Globals scalar (poke the byte at the symbol's address), or array child ([i] → byte at addr+i). Memory writes go through s.ram (bypass MMIO). Refused while running.
setBreakpoints #49, #81 Source-line bps resolved through the .dbg source map. Honors condition / hitCondition (integer) / logMessage (with {expr} interpolation).
setInstructionBreakpoints #49, #81 Address bps ($XX, 0xXX, decimal). Same modifier support as source bps.
setFunctionBreakpoints #82, #81 Symbol-name bps via syms.LookupName. Same modifier support.
dataBreakpointInfo #453 Resolves a hex/decimal address or symbol name to a data-breakpoint id ($XXXX); reports read/write/readWrite access types.
setDataBreakpoints #453 Memory watchpoints. The run loop's AccessWrite/AccessRead hook (chained with the #440 dirty hook) flags a watched access; the run stops with reason data breakpoint after the instruction completes. accessType defaults to write; condition/hitCondition reuse the instruction-bp meta.
loadedSources #84 Lists every file the loaded .dbg references.
source #84 Returns file contents for a previously-listed Source. Basename-matches if the client passes an absolute path.
disassemble #51, #80 Variant-aware via cpu.DisasmCPU. Negative instructionOffset resolves via cpu.WalkBack for pre-context.
readMemory / writeMemory #51 Bypasses MMIO — peripherals don't see debugger pokes.
evaluate #52 Watch / hover / debug-console expressions via internal/expr.
completions #85 Debug-console autocomplete: registers, flag bits, and loaded .dbg symbols.
setExceptionBreakpoints #83 Toggles "pause on BRK" via the brk filter. Run loop checks for $00 opcode before each cpu.Step().
exceptionInfo #83 Describes the last-fired exception (brk with PC).

Custom events

chippy-state (live state streaming, #395)

A server→client custom event pushed during a free-run (continue) so a client refreshes its panels without polling variables every frame. It's chippy-specific — standard DAP clients ignore unknown events, so this is safe to emit on a shared channel. Throttled server-side to ≤60 Hz.

{
  "type": "event",
  "event": "chippy-state",
  "body": {
    "a": 0, "x": 0, "y": 0, "sp": 253, "p": 36,   // raw bytes, not "$XX"
    "pc": 32769,
    "cycles": 12044,
    "halted": false,
    "dirtyRanges": [                       // memory written since the last event
      { "start": 768, "end": 769, "data": "qg==" }   // [start,end) + base64 bytes
    ]
  }
}

dirtyRanges (#440) carries the memory written since the previous event, coalesced into half-open [start, end) spans with the current bytes inline (base64). During a free-run the server arms an AccessWrite hook (cpu.SetAccessHook, #421) that stamps a dirty bitmap; each throttled chippy-state flushes the coalesced spans and clears. The hook composes with a host's own access hook (chained, restored on stop) and is removed when the run ends — zero per-write cost when not running. start + len(data) is authoritative (a span ending at $FFFF wraps end).

The chippy TUI subscribes while running: it refreshes the Registers panel and applies the dirtyRanges deltas to its mirror RAM, so the memory and disassembly panels stay live without a per-frame readMemory. The stopped event still does a full-RAM reconcile as the authoritative final sync. vscode-chippy (or any host) can subscribe for the same live update. Following the Mesen / DAP-extension convention, the event is additive — never required for correctness.

Host debug hooks

For a downstream emulator (e.g. nessy) building an NES-aware debugger on the chippy core, three opt-in extension points keep the core CPU-generic (#419):

Hook Use
AttachConfig.CustomRequestHandler (#416) serve the host's own vendor/command DAP requests over the same connection.
Server.SetHostVars(expr.HostVarResolver) (#433) register host identifiers (e.g. scanline, dot, frame) that conditional-breakpoint / evaluate expressions resolve at eval time — scanline == 30 works against PPU state the 6502 can't see.
Server.SetStopPredicate(func() bool) (#433) a host stop condition checked once per continue-loop iteration; lets the host build NES step granularity (run-to-NMI, step-scanline, step-frame) through the server's pause/ownership model. Arm it, send continue, disarm on the stopped.

cpu.SetAccessHook (#421, access heatmap) and RAM.Freeze (#422, write-suppress) are the matching core-level hooks — see the API reference.

Known gaps

  • attach requires the host process to wire a debuggee via Server.AttachExisting. The cross-process / in-TUI flow is filed at #97.
  • Peripherals aren't snapshotted by reverse-step (see #62).
  • The DAP server is a single session per process. To debug two ROMs at once, run two chippy -dap instances on different ports.

Expression grammar (evaluate, conditional breakpoints)

Same compiler as the TUI:

A == $42
X > 10 && Y != 0
(A & $80) != 0
[$0042] == X
C && !Z
PC >= main

See internal/expr/expr.go for the full operator table.

Troubleshooting

  • No output from a trace launched via DAP. Same as the TUI: pass "tracePath" AND set "stopOnEntry": false (or send continue after launch). The CPU doesn't auto-run on launch.
  • launch returns launch requires a 'rom' argument. Your config is missing the rom field. Path can be absolute or relative to the cwd of the chippy process.
  • Breakpoints come back verified: false. The .dbg source map doesn't cover the requested (file, line). Confirm dbgPath resolves and that the file matches a sym entry's file= field in the .dbg.