Содержит правки из следующих коммитов: - WIP: all-in-one - Janko: cache on - DeZog fix - Revert "drop mlz" - emu/debug/debugcpu.cpp,sinclair/spectrum.cpp: Guarded pointer accessors - sinclair/sprinter.cpp tmkonf - dma delay under investigation - harddisks-shareable.diff - ignore - плагин и скрипты для управления MAME by Claude code (MCP) - много всего - mouse release
13 KiB
CLAUDE.md — MAME Z80 debugging via Claude
Goal
Drive the MAME debugger from Claude (in VS Code) to test Z80/8080 code written to run inside an emulated retro computer. Claude should be able to inspect/modify registers and memory, set breakpoints/watchpoints, single-step, and disassemble.
Two integration paths (decided in prior session)
Path A — MAME built-in GDB stub (-debugger gdbstub)
- The C++ module
src/osd/modules/debugger/debuggdbstub.cppdoes support Z80: shortnamesz80,z80n,z84c015map to a Z80 register set (AF, BC, DE, HL, AF', BC', DE', HL', IX, IY, SP, PC). Also covers 6502 family, 6809, 68000/020/030, ARM7, MIPS, PPC601, nios2, PSX. - NOTE: the Lua plugin
plugins/gdbstubis i386-only. Don't confuse the two. - Launch:
mame <system> -debug -debugger gdbstub -debugger_port <port>(read the actual port from MAME's startup log linegdbstub: listening on ...). - Connect any GDB-remote client (e.g.
gdb-multiarch) or an MCP wrapper over gdb. - Limitations that matter for retro machines: single connection only, flaky reconnect (often must restart MAME per session), and no memory bank paging — sees a flat address space. Bad for ZX Spectrum 128 / MSX / banked CP/M.
Path B — Custom Lua bridge (files in this repo) ← PRIMARY PATH
plugins/mamebridge/(init.lua + plugin.json) +mame_mcp.py. Works for any CPU and reads memory through MAME's live address space, so bank switching is handled correctly.- Former caveat NOW FIXED: the old
mame_bridge.luaautoboot script polled viaadd_machine_frame_notifier, which does not fire while the machine is hard-stopped in the debugger. Themamebridgeplugin instead pumps viaemu.register_periodic(), which is driven byemulator_info::periodic_check()inside the debugger'swhile (is_stopped())loop (src/emu/debug/debugcpu.cpp). So interactive stop→step→inspect loops now work. mame_bridge.luais kept as a fallback for live (running) inspection only.
RESOLVED: banked vs flat memory
The target machine uses banked memory → Path B (Lua bridge) is the primary path. (Path A / gdbstub sees a flat address space and can't follow bank switching, so it's unsuitable here.)
Files
plugins/mamebridge/— the plugin (init.lua + plugin.json). Load with-plugin mamebridge. File-IPC over a shared dir. Pumps viaregister_periodic, so it works while the machine is stopped.mame_bridge.lua— older autoboot version (-autoboot_script). Fallback for live inspection only; goes silent when the debugger hard-stops.mame_mcp.py— FastMCP server (pip install "mcp[cli]"). Talks to the bridge. Unchanged between the two — the file-IPC protocol is identical.
Run (Path B)
mame <system> -debug -plugin mamebridge
claude mcp add mame-z80 -- python ./mame_mcp.py
Debugger-agnostic — VERIFIED under the native macOS Cocoa debugger. The bridge
pump (emu.register_periodic) is driven by emulator_info::periodic_check() in the
core while (is_stopped()) loop (debugcpu.cpp:446), which runs before
wait_for_debugger for ANY OSD debugger module. So it works the same with
-debugger qt, -debugger imgui, or -debugger auto (= Cocoa debugosx on macOS).
On 2026-05-22 all bridge commands were verified live under -debugger auto (Cocoa),
incl. the critical stop/step loop responding with no timeout while hard-stopped.
(Earlier the Cocoa debugger could starve the pump because its drawRect used
NSLayoutManager ~1.7s/redraw on the same main thread; that view is now Core-Text-
based, so the starvation is gone.)
If MAME can't find the plugin, add the repo plugins dir explicitly:
-pluginspath /Users/tolik/Documents/GitHub/mame/plugins.
Env (must match on both sides):
MAME_MCP_DIRIPC dir (default /tmp/mame_mcp)MAME_MCP_CPUCPU tag (default :maincpu)MAME_MCP_SNAP_DIRscreenshot dir (default /tmp/mame_snap, cleared on reboot)MAME_MCP_TIMEOUTreply wait seconds (Python side, default 10)
MCP tools exposed
read_registers, read_memory, read_logical_memory, read_vram, read_share,
list_shares, write_memory, set_breakpoint (with optional condition expr),
clear_breakpoint, list_breakpoints, set_watchpoint, clear_watchpoint, step,
step_over, step_out, resume, pause, status, disassemble, screenshot,
list_ports, press_key, type_text, move_mouse, click_mouse, press_input,
set_input, debugger_command (raw console-command escape hatch).
Address args accept "0xC000", bare hex "C000", or decimal "49152" (the plugin's
parse_addr normalises all three).
screenshot (bridge cmd snap) saves a PNG of the active screen and returns its
path; open that path to view the screen. Snaps go to MAME_MCP_SNAP_DIR
(default /tmp/mame_snap, cleared on reboot). The image is the last drawn frame,
so while hard-stopped resume briefly before snapping for a fresh frame.
Reading the screen for mouse navigation (scrpix) — what works & what's hard
scrpix x y w h(bridge) reads RENDERED screen pixels viascreen:pixel(x,y)(4-hex pen each, row-major, max 8192 px). This is the video output decoded from VRAM by the hardware — correct screen coords, no manual tile/4bpp/buffer math.- Raw VRAM is much harder: graphics mode is TILE-BASED (16x8 tiles, 4-byte
descriptors via
as_mode), 4bpp (screen_x ≈ 2·vram_byte_x), TWO panels in one VRAM, and DOUBLE-BUFFERED — active descriptor set chosen by rgmod (read withcmd print (ib@C9)); the UI list is in the active buffer (+0x20000) while the mouse CURSOR is drawn in the OTHER buffer. So raw-VRAM diffs gave false cursor coords. Prefer scrpix. - Finding the cursor: text AND cursor are both white (pen 0xFFFF=65535), so the cursor can't be isolated by colour — only by DIFF (move mouse a little, diff two scrpix grabs; changed pixels = cursor, click point = top-left of them).
- List structure (C:\ZX): text rows on an 8px pitch; left panel columns ~x48-150 (col1) and ~x166-220 (col2 with pent_*/sprinter). sprinter.zx ≈ row6 of col2.
- NOT SOLVED yet: robust closed-loop pointing. The diff-based find_cursor is flaky (probe move + timing/buffer issues lose the cursor) and the mouse->pixel scale drifts, so an automated click on a specific small item isn't reliable yet. Keyboard navigation (press_key, точные одиночные нажатия) is the reliable path.
Sprinter memory & video map (from mame/sinclair/sprinter.cpp, verified live)
CPU is z84c015 with THREE spaces: program (mem), io, opcodes (fetch). Memory is banked, so reading needs care.
Banking — 64K Z80 logical → physical pages:
- 4 windows of 16K. Crucially the program space is wider than 64K: the windows
live at program 0x10000+ (
map_mem, sprinter.cpp:1448): win0 Z80 0x0000-0x3FFF → program 0x10000; win1 0x4000→0x14000; win2 0x8000→0x18000; win3 0xC000→0x1C000. - Reading
mem L(raw program 0..0xFFFF) hits bootstrap_r (sprinter.cpp:1185), which forwards to 0x10000|L after the FPGA bitstream loads but returns fastram while loading — so it's unreliable early. Useread_logical_memory/lmem(reads 0x10000|L) for the Z80's real view. Verified: lmem == mem(0x10000|L) == dasm at the same address. - Physical pages are m_ram (default 64M) indexed by page<<14 (configure_entries,
sprinter.cpp:1577). The page for each window is computed in
update_memory(sprinter.cpp:327) from a tangle of ports (m_pn/7FFD, m_sc/1FFD, m_cnf, m_dos, m_rom_rg, ...) via the DCP table m_ram_pages. When a window's page&0xF0==0x50 it is the VRAM aperture: phys = (0x50<<14)+m_port_y*1024+(offset&0x3FF).
Video (VRAM = 256K share ":vram", read with read_vram):
- Mode select: m_conf (rare Game Config) → screen_update_game, else screen_update_graph (sprinter.cpp:404). Modes: 320x256x256, 640x256x16, Spectrum, text. Per-16x8-tile 4-byte "mode descriptor" at as_mode(a,b)=vram+(1+a2+0x80(m_rgmod&1))1024+0x300+b4 (sprinter.cpp:554).
- Pixel fetch (draw_tile, sprinter.cpp:453): color = vram[(y+((dy-hold.y)&7>>lowres))*1024 + x + ((dx-hold.x)&15>>(1+lowres))], x=(mode[0][0:4]<<6)|(mode[1][0:3]<<3), y=mode[1][3:8]<<3. 8bpp if mode[0] bit5 else 4bpp (2 px/byte). Palette base = mode[0][6:8]<<8.
- Palette lives in VRAM: writes to laddr>=0x3E0 set pen (offset[2:5]*256 + offset>>10) = RGB triple at offset&~3 (vram_w sprinter.cpp:1287). 256 pens * 8 palettes. Text pens are 0x400+.
- Easiest "see the screen": just use
screenshot. Use read_vram for programmatic checks (did the program write expected pixels/palette without rendering).
Breakpoints / watchpoints by region:
- PC breakpoints (
set_breakpoint) take logical 0..0xFFFF (fetch space) — works regardless of which physical page is banked in. - Watchpoints default to program space:
wpset ADDR,LEN,rw. For a specific Z80 window use the 0x10000+ address; for raw logical use the debugger directly. The bridge now sets bp/wp via the NATIVE Lua debugger API (cpu().debug:bpset/wpset/...), not console strings.wp/set_watchpointtake an optional space:wp ADDR LEN ACCESS [program|data|io|opcodes](→cpu().debug:wpset(cpu().spaces[space], type, addr, len)). NOTE: the native wpset address is LITERAL in the chosen space (no bank translation) — to watch a Z80 window in program space pass the 0x10000+ address (e.g. 0x18000 = window 2), exactly like write_memory/mem. (The old consolewpset 0x8000quirkily relocated to 0x18000; native is literal/predictable.) - To catch BANK SWITCHES, set an IO watchpoint on the paging ports. Current MAME
uses a SPACE-SPECIFIC command —
wpisetfor io space (the oldwpset ADDR,1,w,1,1,iform is rejected as "too many parameters"). E.g.set_watchpoint("0xc1",1,"w",space="io"), or via debugger_command:wpiset 0xc1,1,w(port 0xC1=m_pn) or 0xC0 (m_sc), 0xC4 (m_port_y), 0xC5 (m_rgmod), 0xC6 (m_cnf). (wpset=program,wpdset=data,wpiset=io.) NOTE: ports written internally via the FPGA/DCP — e.g. rgmod 0xC9 — are NOT seen by an io watchpoint, since they aren't reached by a Z80 OUT; an io wp only catches actual IN/OUT on that port. - Note: m_pages/m_pn/etc. are private C++ members, NOT exposed to Lua. To inspect current banking, watch the ports or read the DCP copies in m_ram.
Sprinter input injection (verified live)
Input is only processed while the machine is RUNNING — frame notifiers and the
natkeyboard timer don't tick while hard-stopped. So resume before sending input
(and pause after if needed). The plugin holds each press for N frames via a
frame notifier that re-applies set_value(1) every frame (MAME re-polls per frame).
Two of everything. On the real host one keypress goes to BOTH keyboards and
mouse motion to BOTH mice, so press_key/move_mouse/click_mouse drive both:
- Keyboards: PC PS/2
:kbd:ms_naturl:P1.0..P2.7(what Flex Navigator and most firmware reads — verified: cursor keys/Tab move the UI) AND ZX matrix:IO_LINE0..7.press_keymaps named keys to both where an equivalent exists. - Mice: Kempston
:mouse_input1/2(X/Y mask 0xFF) + buttons:mouse_input3, AND RS232 COM:rs232:microsoft_mouse:X/Y(mask 0xFFF) +:BTN. Mouse axes are relative, so motion is held several frames to accumulate. - Joysticks:
:JOY1/:JOY2(e.g. A=0x400, B=0x010, Up=0x208, Down=0x104, Left=0x002, Right=0x001) via press_input.type_textuses natkeyboard but targets only one keyboard and needs in_use; for the firmware UI preferpress_key. Uselist_portsto rediscover tags/masks.
Environment gotchas (verified on the sprinter machine)
plugins/here is symlinked from~/Documents/MAME/plugins, which is the activepluginspath, so editing the repo files updates the running plugin.- Run with the real config (see
~/Documents/MAME/Debug.sh):-bios devplus the disk/floppy images. Without-bios devthe defaultsp2kBIOS ROM is missing and the machine refuses to start. - The
portnameplugin used the removedemu.register_start; it now usesemu.add_machine_reset_notifier. If it ever crashes the boot again with "attempt to call a nil value", that's the regression to look at (or just run with-noplugin portname). - Only run ONE
mamedat a time against a givenMAME_MCP_DIR; a second instance fails to start but a stale first instance keeps answering with old plugin code.
Next actions
Confirm banked vs flat memory.DONE — banked.TurnDONE — seemame_bridge.luainto a plugin (init.lua + plugin.json) for reliable stop/step.plugins/mamebridge/.- Smoke test: load Z80 program,
set_breakpointat entry,resume, read regs. Then verify the stop/step loop: at a breakpoint,statusmust replystate=stopwithout timing out, andstep+read_registersmust advance PC while staying stopped.