The ADP debugger
The Advanced Debugging Protocol (ADP) is a plain-text, line-oriented TCP protocol on port 11001. Send a command line, read the text reply. Any socket client can drive the machine with no recompile — ideal for scripting and for quick, editor-independent inspection. It’s the primary, verified debugging surface in Bizmuth (Rhai scripting is built on top).
The core is single-threaded: the CPU, video, memory and debugger all run on one thread, and commands are applied between instructions — never mid-instruction — so state you read is always consistent. Numeric arguments accept decimal or 0x/$ hex.
Connecting
The ADP server is on by default (change the port with --adp-port, or pick protocols with --debug-protocol adp|dzrp|both|none). Connect however you like:
nc localhost 11001 # interactive: type a command, read the replyhelp (or ?) prints the live command list; an unknown verb replies unknown command '…' (try 'help').
Message framing. Every message block — the banner, a command reply, or an async event — ends with a blank line, so a client can frame a multi-line reply (mem, disasm, reg, bp list) by reading lines until an empty one. A first line beginning with * is an unsolicited async event, not a reply. For automated/CI use, --headless runs the server with no window until the process is killed.
Execution control
| Command | Reply / effect |
|---|---|
c / continue | resume free-running execution → running |
pause | stop at the current instruction (snapshots for step-back) → paused pc=XXXX |
s / step [n] | single-step n instructions (default 1) → pc=XXXX — step-into |
next / stepover | step over a CALL/RST/block-op (run to the following instruction); a plain single-step otherwise |
sb / stepback [n] | reverse-step n snapshots → stepped back; pc=XXXX (N states left) |
runto / run <addr> | run to <addr> via a one-shot breakpoint (auto-removed when it fires) |
finish / fin | step out — run to the current subroutine’s return address |
frame | print the frame counter → frame N |
pctrace <file> | from now on, append (pc, sp) per instruction to <file> (gate a deep trace to a region) |
Step-back replays saved full-machine snapshots — exact, not heuristic, but only reaches as far back as the snapshot history (snapshots are taken before each step/pause, not during free-run). no earlier state in history means you free-ran past the recorded window — re-run and step in.
Async stop events. While the machine is free-running (after continue/runto/finish/next), a stop is pushed to the client as an unsolicited line prefixed with * — so you learn of a breakpoint hit without polling: *stopped pc=XXXX reason=bp id=N. The leading * marks it apart from command replies, so a DAP / IDE bridge maps it straight to a DAP stopped event.
Inspection
| Command | Reply / effect |
|---|---|
state | one-line CPU + paging summary: pc=.. sp=.. af=.. bc=.. de=.. hl=.. ix=.. iy=.. i=.. im=.. iff=.. |
reg [name[=val]] | dump all registers / read one / set one. Names: af bc de hl ix iy sp pc af' bc' de' hl' i r |
mem <addr> [len] | hex-dump len bytes (default 16, max 0x1000) — paging-resolved (what the CPU sees) |
mem write <addr> <hex…> | poke bytes (paging-resolved), e.g. mem write 8000 DE AD BE EF |
d / disasm <addr> [n] | disassemble n instructions (default 8), including Z80N opcodes |
bt / backtrace | shadow call/interrupt stack (needs debugging enabled early) |
isr | inside an interrupt handler? → in_isr=.. (pc=..) |
overlays / ov | active memory overlays (bootrom / DivMMC automap / config-mode / alt-ROM / MMU slots) — which ROM mem/disasm show at $0000–$3FFF |
nr <reg> | read a nextreg (hex) → nr XX = XX (raw XX) — the CPU read-back and the stored raw byte (they can differ) |
get <path> | read any serialized state field by dotted path (see below) |
mem / disasm resolve through the live MMU and overlays (bootrom, Layer 2 paging, config mode, DivMMC, Multiface), so they show exactly what the CPU would fetch — not raw bank RAM.
Video, sprites & DMA
See Inspecting hardware for these in depth.
| Command | Reply / effect |
|---|---|
sprites / spr | visible sprites: raw attributes + resolved anchor/relative position, transform and pattern |
sprslots <start> [end] | sprite pattern slots start..=end as JSON (256 bytes/slot) |
sprram <path.png> | render sprite pattern RAM to a colour PNG (live palette) |
layer / ly [name] [on|off] | list layer visibility, or isolate a layer (ula|lores|tilemap|layer2|sprites; toggles if on/off omitted) |
screenshot / shot <path.png> [layers] | save the frame; layers = comma list of ula,lores,tilemap,layer2,sprites to capture only those |
bankshot <page> <path.png> | render a physical RAM page as the ULA/Timex screen (view an off-screen buffer bank) |
dma | recent DMA transfers (src[mode] dest[mode] count + I/O flags) |
copper / cop | copper state — mode / exec address + the decoded WAIT/MOVE/NOP list |
Breakpoints & watchpoints
| Command | Effect |
|---|---|
bp add pc <addr> [if <cond>] | add a PC breakpoint, optionally conditional |
bp add pc <pg>:<off> | page-qualified (physical) breakpoint for banked code — fires wherever the CPU executes at physical 8 KB page <pg>, offset <off> (both hex, off in 0000..1FFF), e.g. bp add pc 05:1118. Banking-robust; maps to a debug-info (page, off) location for source-level debugging. |
bp add mem <addr> [r|w|rw] | add a memory watchpoint — break on read / write / either (default rw) |
bp list | list breakpoints with their labels |
bp del <id> | remove one |
bp clear | remove all |
The optional if <cond> is only evaluated once the address already matched, so the hot path stays a cheap compare — see Conditional breakpoints for the full condition language (registers, flags, memory derefs, nr[]/nrraw[] nextregs).
State & snapshots
| Command | Reply / effect |
|---|---|
savestate <file> | write a full compressed savestate (AZST) → saved <file> |
loadstate <file> | restore a full savestate → loaded <file> |
loadsnapshot / loadsnap <file> | load a .sna/.snx/.z80/.nex via the host loader, then pause → loaded <file>; paused pc=XXXX |
State round-trips faithfully; behaviour after a load can still diverge for programs that read the host-backed RTC (live wall-clock). The frame counter resumes at the saved frame, so on_frame handlers keyed off absolute frame numbers should use load-relative offsets.
Paths with spaces must be double-quoted — savestate "/tmp/my save.azst" (inside quotes, \ escapes the next char). The tokenizer is quote-aware, so every path-bearing command (savestate, loadstate, loadsnapshot, screenshot, script load) handles spaces.
Input injection
Drive the machine with no window — navigate the NextZXOS browser and launch programs headlessly.
| Command | Reply / effect |
|---|---|
key <tok> [frames] | inject a keypress held frames frames (default 8), then release → pressed <tok> for N frames |
enter / down / up / left / right / space / y / n | shorthand for key <tok> |
Tokens map to the ZX key matrix; cursor keys use the NextZXOS browser mapping (down = key 6, up = key 7). Keys only advance while the engine is running (continue), not while paused — e.g. down×N then enter to launch a browser entry.
Scripting & session
| Command | Reply / effect |
|---|---|
script load <file> | load a Rhai script → loaded <file>; handlers: [on_frame, …] |
script eval <expr> | evaluate a Rhai expression against live state |
script list | list active handlers |
help / ? | command list |
q / quit | close this connection (the machine keeps running) |
See Rhai scripting for the scripting layer.
get — state interrogation
get <path> walks the machine’s serialized state tree — the same fields the savestate captures — and prints the addressed value as JSON. Paths are dotted, with [index] for arrays:
get bus.memory.mmu.slots → [255,255,10,11,4,5,0,1] (the 8 MMU slot banks)
get bus.next_regs.regs[192] → 8 ($C0 raw, decimal index 192)
get bus.ula.frame → 1386 (the ULA frame counter)
get cpu → {…} (the whole CPU sub-object)Useful roots: cpu, bus, bus.memory.mmu.slots, bus.next_regs.regs (the raw 256-entry nextreg array — the stored/written byte), bus.layer2.*, bus.lores.*, bus.sprites.*. A missing field replies no such field: <name> and an out-of-range index index N out of range, so you can probe the tree incrementally. Large RAM blobs are elided.
Example session
> state
pc=0C8F sp=5BF3 af=0344 bc=02DF de=7D30 hl=5C3B ix=DBD0 iy=5C3A i=09 im=1 iff=11
> nr 22
nr 22 = 00 (raw 00)
> bp add pc 0x8000 if a == 0x1e
bp #0 added
> mem 0x4000 8
4000 00 00 FE 01 FE 01 FE 00
> screenshot /tmp/frame.png layer2,sprites
saved screenshot (640x256) to /tmp/frame.png
> c
runningFor automation, layer conditions and reacting to hardware events, build on ADP with Rhai scripting.