Содержит правки из следующих коммитов: - 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
451 lines
33 KiB
Markdown
451 lines
33 KiB
Markdown
# Управление эмулятором MAME из Claude — переносимый онбординг
|
||
|
||
> **Назначение этого файла.** Подгрузи его в новую сессию Claude (в любом проекте),
|
||
> и я сразу умею управлять эмулятором MAME (на примере машины **Sprinter**, CPU
|
||
> z84c015): читать/писать регистры и память (с учётом банкирования), ставить
|
||
> брейк-/вотчпойнты, шагать, дизассемблировать, **видеть экран** и **управлять**
|
||
> машиной (клавиатура, мышь, джойстик).
|
||
>
|
||
> Как подгрузить: либо «прочитай файл `/Users/tolik/Documents/GitHub/mame/MAME_MCP_GUIDE.md`
|
||
> и действуй по нему», либо скопируй этот файл в `CLAUDE.md` нового проекта.
|
||
>
|
||
> Все пути абсолютные — файл самодостаточен и не зависит от текущего проекта.
|
||
|
||
---
|
||
|
||
## 0. TL;DR — как поднять связку за 3 шага
|
||
|
||
```bash
|
||
# 1) Запустить MAME с дебаггером и плагином-мостом (см. полный конфиг в Debug.sh):
|
||
cd /Users/tolik/Documents/MAME && ./Debug.sh # содержит -debug -debugger auto (на macOS = Cocoa)
|
||
# но в Debug.sh нужно дописать в строку запуска: -plugin mamebridge
|
||
# (без него мост не поднимется). Полный минимум, если запускать вручную:
|
||
# ./mame sprinter -mouse -kbd ms_naturl,bios=sp2k -bios dev -debug -debugger auto -plugin mamebridge <диски...>
|
||
# Мост debugger-agnostic: одинаково работает с -debugger qt | imgui | auto(Cocoa).
|
||
# Проверено вживую под Cocoa (2026-05-22): все команды + цикл stop/step без таймаутов.
|
||
|
||
# 2) Зарегистрировать MCP-сервер в Claude Code (один раз на проект):
|
||
claude mcp add mame-z80 -- python /Users/tolik/Documents/GitHub/mame/src/mame_mcp.py
|
||
|
||
# 3) Готово. В сессии доступны MCP-инструменты read_registers, read_memory, step, scrpix и т.д.
|
||
```
|
||
|
||
Если MAME не находит плагин: добавь явно `-pluginspath /Users/tolik/Documents/GitHub/mame/plugins`.
|
||
|
||
**Остановка MAME:** НИКОГДА не `kill`/`pkill`. Слать debugger-команду `exit`
|
||
(через мост: `debugger_command("exit")` или просто положить запрос `cmd exit`).
|
||
Ответа не будет — процесс завершится, это нормально.
|
||
|
||
---
|
||
|
||
## 1. Архитектура связки
|
||
|
||
```
|
||
Claude (MCP-клиент)
|
||
│ вызывает MCP-инструменты
|
||
▼
|
||
mame_mcp.py (FastMCP-сервер, stdio) pip install "mcp[cli]"
|
||
│ файловый IPC: пишет req_<id>.txt, ждёт resp_<id>.txt в общей папке
|
||
▼
|
||
plugins/mamebridge/init.lua (плагин MAME)
|
||
│ читает запрос, исполняет в дебаггере MAME, атомарно пишет ответ
|
||
▼
|
||
MAME debugger ──> эмулируемая машина (Sprinter / любой CPU)
|
||
```
|
||
|
||
- **IPC**: текстовые файлы `req_<id>.txt` / `resp_<id>.txt` в общей папке
|
||
(по умолчанию `/tmp/mame_mcp`). Запись атомарная (`.tmp` + rename).
|
||
- **Почему плагин, а не `-autoboot_script`**: старый `mame_bridge.lua` опрашивал
|
||
IPC через `emu.add_machine_frame_notifier`, который **молчит, пока машина
|
||
hard-stopped** в дебаггере → интерактивный stop→step→inspect зависал. Плагин
|
||
качает опрос через **`emu.register_periodic()`**, который вызывается из
|
||
`emulator_info::periodic_check()` ВНУТРИ цикла дебаггера
|
||
`while (is_stopped())` (src/emu/debug/debugcpu.cpp). → шаги/инспекция в стопе
|
||
работают.
|
||
- **Совместимость**: протокол IPC у `mame_bridge.lua` (fallback) и плагина
|
||
идентичен — `mame_mcp.py` один и тот же.
|
||
|
||
### Файлы (всё в /Users/tolik/Documents/GitHub/mame)
|
||
| Файл | Роль |
|
||
|------|------|
|
||
| `plugins/mamebridge/init.lua` | плагин-мост (вся логика команд, ввод, scrpix) |
|
||
| `plugins/mamebridge/plugin.json` | манифест, `"name":"mamebridge"`, `"start":"false"` |
|
||
| `src/mame_mcp.py` | FastMCP-сервер, объявляет MCP-инструменты |
|
||
| `src/mame_bridge.lua` | старый autoboot-вариант, fallback для live-инспекции |
|
||
| `src/CLAUDE.md` | подробная проектная документация (источник истины) |
|
||
|
||
### Ключевое про окружение
|
||
- `plugins/` — это **симлинк** из активного `pluginspath`:
|
||
`/Users/tolik/Documents/MAME/plugins → /Users/tolik/Documents/GitHub/mame/plugins`.
|
||
→ правки файлов плагина в репозитории **сразу** живые (Lua интерпретируется),
|
||
пересборка MAME НЕ нужна.
|
||
- Запускать **только ОДИН** `mame` против одной `MAME_MCP_DIR`. Второй инстанс не
|
||
стартует, а застрявший первый продолжит отвечать СТАРЫМ кодом плагина — путаница.
|
||
- Запускать с реальным конфигом (`-bios dev` + диски), см. `~/Documents/MAME/Debug.sh`.
|
||
Без `-bios dev` дефолтный BIOS `sp2k` отсутствует и машина не стартует.
|
||
- Плагин `portname` когда-то ронял загрузку (`register_start` удалён из API).
|
||
Если снова `attempt to call a nil value` на старте — это он; запусти с
|
||
`-noplugin portname` либо проверь, что он использует `add_machine_reset_notifier`.
|
||
|
||
### Переменные окружения (должны совпадать на обеих сторонах)
|
||
| Переменная | Что | Дефолт |
|
||
|------------|-----|--------|
|
||
| `MAME_MCP_DIR` | папка IPC | `/tmp/mame_mcp` |
|
||
| `MAME_MCP_CPU` | тег CPU | `:maincpu` |
|
||
| `MAME_MCP_SNAP_DIR` | папка скриншотов | `/tmp/mame_snap` (чистится при ребуте) |
|
||
| `MAME_MCP_TIMEOUT` | таймаут ожидания ответа (сторона Python) | `10` сек |
|
||
|
||
---
|
||
|
||
## 2. MCP-инструменты (что я вызываю)
|
||
|
||
Адреса принимают `"0xC000"`, голый hex `"C000"` или десятичное `"49152"`
|
||
(нормализует `parse_addr`).
|
||
|
||
**CPU / память**
|
||
- `read_registers()` — регистры Z80/8080 (PC, SP, AF, BC, DE, HL, IX, IY, ...).
|
||
- `read_memory(address, length=16)` — СЫРОЕ программное пространство (без банков).
|
||
На Sprinter окна Z80 живут в program 0x10000+ — для логики используй lmem.
|
||
- `read_logical_memory(address, length=16)` — как видит Z80 через банки
|
||
(логический 0x0000–0xFFFF → program `0x10000|addr`). **Обычно нужен этот.**
|
||
- `read_vram(address, length=16)` — напрямую из видеоОЗУ (share `:vram`, 256K),
|
||
минуя банкинг. Для пиксельных данных, тайлов/дескрипторов, палитры.
|
||
- `read_share(name, address, length=16)` — из именованного share (`vram`,
|
||
`fastram`, ...). См. `list_shares()`.
|
||
- `list_shares()` — список share/region (теги + размеры).
|
||
- `write_memory(address, hex_bytes)` — запись в program space (`"3E01CD0500"`).
|
||
Для логического вида Z80 пиши по `0x10000|addr`.
|
||
|
||
**Точки останова / шаги**
|
||
- `set_breakpoint(address, condition="")` — PC-брейк; condition — выражение
|
||
дебаггера (`"A==0"`). Возвращает индекс.
|
||
- `clear_breakpoint(index)`, `list_breakpoints()`.
|
||
- `set_watchpoint(address, length, access="w", space="program")` — `r`/`w`/`rw`;
|
||
`space`=`program`(умолч.)|`data`|`io`|`opcodes` (напр. `space="io"` ловит Z80
|
||
IN/OUT по порту). Bridge-verb: `wp ADDR LEN ACCESS [space]`. Ставится нативно
|
||
(`cpu().debug:wpset`); адрес ЛИТЕРАЛЬНЫЙ в пространстве — для Z80-окна в program
|
||
давай 0x10000+ (напр. 0x18000 = окно 2).
|
||
- `clear_watchpoint(index)`.
|
||
- `step(count=1)`, `step_over(count=1)` (CALL до конца), `step_out()`.
|
||
- `resume()` (cont), `pause()`, `status()` (running/stopped + PC).
|
||
- `disassemble(address, num_bytes=32)`.
|
||
|
||
**Экран**
|
||
- `scrpix(x,y,w,h)` — ⭐ ОСНОВНОЙ способ «смотреть»: читает ОТРИСОВАННЫЕ пиксели
|
||
через `screen:pixel(x,y)`, по 4 hex-символа на пиксель (пен), row-major,
|
||
до 8192 px. Это видеовыход, декодированный железом → правильные экранные коорд.,
|
||
без ручной возни с тайлами/4bpp/буфером.
|
||
- `screenshot(name="")` — PNG активного экрана, возвращает путь. **Последнее
|
||
средство** (дорого по токенам, коорд. «на глаз»). Снимок = последний
|
||
отрисованный кадр; в hard-stop сначала ненадолго `resume()` для свежего кадра.
|
||
|
||
**Ввод (клавиатура/мышь/джойстик)** — машина должна быть RUNNING (`resume` перед вводом)
|
||
- `list_ports()` — порты и поля (тег, маска, имя).
|
||
- `press_key(key, frames=3)` — нажать ИМЕНОВАННУЮ клавишу на N кадров с
|
||
авто-отпусканием. Бьёт по ОБЕИМ клавиатурам (PC + ZX), где есть эквивалент.
|
||
- `type_text(text, coded=False)` — печать через natkeyboard; если `coded`,
|
||
понимает `{ENTER}` и пр. (но НЕ курсорные стрелки). Для UI прошивки лучше press_key.
|
||
- `move_mouse(dx, dy, frames=3)` — двигать ОБЕ мыши (Kempston + RS232 COM),
|
||
относительные оси накапливаются за кадры.
|
||
- `click_mouse(button="left", frames=3)` — клик (left/right/middle) на обеих мышах.
|
||
- `press_input(port, mask, frames=2)` — низкоуровневый цифровой ввод
|
||
(`press_input(':JOY1','0x400')`).
|
||
- `set_input(port, mask, value, frames=0)` — задать поле (analog/digital).
|
||
- `debugger_command(raw)` — ⭐ escape-hatch: любая команда консоли дебаггера
|
||
(`"print pc"`, `"print (ib@C9)"`, `"trace t.log"`, `"exit"`).
|
||
|
||
---
|
||
|
||
## 3. Чтение портов эмулируемой машины
|
||
|
||
Чтобы узнать значение, которое выдаёт порт: в консоли дебаггера
|
||
`print (ib@C9)` (читает io-байт порта 0xC9, напр. rgmod). Через мост:
|
||
`debugger_command("print (ib@C9)")`. `ib@<addr>` = io-space byte по адресу.
|
||
|
||
**Не путать внешние и внутренние порты Sprinter:**
|
||
- **Внешние** — порты, с которыми Z80 работает через OUT/IN.
|
||
- **Внутренние** — порты конфигурации Altera FPGA, маппятся на внешние через
|
||
таблицу DCP (decoded control ports).
|
||
|
||
Активный двойной буфер экрана выбирается портом **rgmod**.
|
||
|
||
---
|
||
|
||
## 4. Sprinter: карта памяти и видео (проверено вживую по sinclair/sprinter.cpp)
|
||
|
||
CPU **z84c015**, ТРИ пространства: program (mem), io, opcodes (fetch).
|
||
Память **банкируемая** — читать с осторожностью (поэтому путь B/Lua-мост, а не
|
||
gdbstub: gdbstub видит плоское адресное пространство и не следит за банками).
|
||
|
||
**Банкинг — 64K логических Z80 → физические страницы:**
|
||
- 4 окна по 16K. Program-пространство ШИРЕ 64K: окна живут в program 0x10000+
|
||
(`map_mem`, sprinter.cpp:1448): win0 Z80 0x0000–0x3FFF → program 0x10000;
|
||
win1 0x4000→0x14000; win2 0x8000→0x18000; win3 0xC000→0x1C000.
|
||
- `mem L` (сырой program 0..0xFFFF) идёт в `bootstrap_r` (sprinter.cpp:1185),
|
||
который форвардит в `0x10000|L` *после* загрузки FPGA-битстрима, а пока грузится
|
||
— отдаёт fastram (ненадёжно на раннем этапе). Используй `lmem` (читает
|
||
`0x10000|L`) для реального вида Z80. Проверено: lmem == mem(0x10000|L) == dasm.
|
||
- Физ. страницы — `m_ram` (по умолчанию 64M), индекс `page<<14`
|
||
(configure_entries, sprinter.cpp:1577). Страница окна считается в
|
||
`update_memory` (sprinter.cpp:327) из кучи портов (m_pn/7FFD, m_sc/1FFD, m_cnf,
|
||
m_dos, m_rom_rg, ...) через DCP-таблицу `m_ram_pages`. Если `page&0xF0==0x50` —
|
||
это апертура VRAM: `phys = (0x50<<14)+m_port_y*1024+(offset&0x3FF)`.
|
||
|
||
**Видео (VRAM = 256K share `:vram`, читать через `read_vram`):**
|
||
- Выбор режима: `m_conf` (Game Config) → screen_update_game, иначе
|
||
screen_update_graph (sprinter.cpp:404). Режимы: 320x256x256, 640x256x16,
|
||
Spectrum, text.
|
||
- Per-16x8-тайл 4-байтный «mode descriptor»:
|
||
`as_mode(a,b)=vram+(1+a*2+0x80*(m_rgmod&1))*1024+0x300+b*4` (sprinter.cpp:554).
|
||
- Пиксель (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 если
|
||
mode[0] bit5, иначе 4bpp (2 px/байт). База палитры `= mode[0][6:8]<<8`.
|
||
- Палитра — в VRAM: запись по `laddr>=0x3E0` ставит пен
|
||
`(offset[2:5]*256 + offset>>10)` = RGB-тройка по `offset&~3` (vram_w
|
||
sprinter.cpp:1287). 256 пенов × 8 палитр. Текстовые пены `0x400+`.
|
||
|
||
**Брейк-/вотчпойнты по регионам:**
|
||
- PC-брейки берут логический 0..0xFFFF (fetch-пространство) — независимо от банка.
|
||
- Вотчпойнты по умолчанию в program space: `wpset ADDR,LEN,rw`. Для конкретного
|
||
окна Z80 — адрес 0x10000+.
|
||
- Поймать ПЕРЕКЛЮЧЕНИЕ БАНКОВ — io-вотчпойнт на порты пейджинга. В текущем MAME
|
||
команда ЗАВИСИТ ОТ ПРОСТРАНСТВА: `wpiset` для io (старая форма
|
||
`wpset ADDR,1,w,1,1,i` отвергается — "too many parameters"). Напр.
|
||
`debugger_command("wpiset 0xc1,1,w")` (порт 0xC1=m_pn) или 0xC0 (m_sc),
|
||
0xC4 (m_port_y), 0xC5 (m_rgmod), 0xC6 (m_cnf). (`wpset`=program, `wpdset`=data,
|
||
`wpiset`=io.) ВАЖНО: порты, которые пишутся внутренне через FPGA/DCP (напр. rgmod
|
||
0xC9), io-вотчпойнт НЕ ловит — туда нет Z80-команды OUT; io-wp ловит только
|
||
реальные IN/OUT по этому порту.
|
||
- `m_pages/m_pn/...` — приватные C++ члены, в Lua НЕ проброшены. Чтобы понять
|
||
текущий банкинг — следи за портами или читай копии DCP в m_ram.
|
||
|
||
---
|
||
|
||
## 5. Ввод в Sprinter (проверено вживую)
|
||
|
||
**Ввод обрабатывается только пока машина RUNNING** — frame-нотифаеры и таймер
|
||
natkeyboard не тикают в hard-stop. Значит `resume` ПЕРЕД вводом (и `pause` после,
|
||
если надо). Плагин держит каждое нажатие N кадров, переустанавливая `set_value(1)`
|
||
каждый кадр (MAME перечитывает поля покадрово), и сам отпускает по номеру кадра.
|
||
|
||
**Всё в ДВУХ экземплярах.** На реальном хосте одно нажатие идёт в ОБЕ клавиатуры,
|
||
движение — в ОБЕ мыши. Поэтому `press_key`/`move_mouse`/`click_mouse` бьют по обеим:
|
||
- **Клавиатуры**: PC PS/2 `:kbd:ms_naturl:P1.0..P2.7` (его читает Flex Navigator и
|
||
большинство прошивок — проверено: курсор/Tab двигают UI) И ZX-матрица
|
||
`:IO_LINE0..7`. `press_key` маппит имена в обе, где есть эквивалент.
|
||
- **Мыши**: Kempston `:mouse_input1/2` (X/Y маска 0xFF) + кнопки `:mouse_input3`, И
|
||
RS232 COM `:rs232:microsoft_mouse:X/Y` (маска 0xFFF) + `:BTN`. **В Flex Navigator
|
||
читается COM-мышь, а не Kempston.** Оси относительные → движение держим
|
||
несколько кадров для накопления.
|
||
- **Джойстики**: `:JOY1`/`:JOY2` (A=0x400, B=0x010, Up=0x208, Down=0x104,
|
||
Left=0x002, Right=0x001) через `press_input`.
|
||
|
||
Имена клавиш в `press_key`: a–z, 0–9, enter, space, esc, tab, backspace,
|
||
up/down/left/right, home, end, pgup, pgdn, ins, del, f1–f12, shift, ctrl, alt,
|
||
symbolshift и символы `; ' / . , - = [ ] \ ` `.
|
||
|
||
**ВАЖНО про скорость и «залипание»** (объяснение пользователя):
|
||
> «Мышка и клавиши летят в буферы SIO процессора на скорости 1200 бод, поэтому
|
||
> зажатие клавиши на несколько кадров может сгенерировать много повторов.»
|
||
|
||
PS/2 typematic delay ≈ 60 опросов @60Гц ≈ 1 сек. Поэтому:
|
||
- для ОДИНОЧНОГО нажатия держи мало кадров (frames=2..3) — иначе авто-повтор
|
||
«настрочит» символ десятки раз и список/курсор «улетит»;
|
||
- между одиночными нажатиями есть естественная пауза из-за 1200 бод — это нормально,
|
||
«быстрее» = риск повторов.
|
||
|
||
ZX-токены: в TR-DOS работают токены клавиатуры как в BASIC спектрума, поэтому
|
||
**`LIST` — это ОДНА клавиша `K`** (IO_LINE6, маска 0x04).
|
||
|
||
`type_text` использует natkeyboard, но целится в ОДНУ клавиатуру и требует in_use;
|
||
для UI прошивки предпочитай `press_key`. natkeyboard НЕ умеет курсорные стрелки.
|
||
Перед natkeyboard выбери целевую клавиатуру (в плагине — команда `kbsel ms_naturl`).
|
||
`list_ports` — чтобы заново найти теги/маски.
|
||
|
||
---
|
||
|
||
## 6. Как «смотреть» на экран — три метода и что выбирать
|
||
|
||
Оценка по скорости / точности / токенам:
|
||
|
||
| Метод | Скорость | Точность коорд. | Токены | Когда |
|
||
|-------|----------|-----------------|--------|-------|
|
||
| **scrpix** (`screen:pixel`) | средняя | **пиксель-точно** (декодировано железом) | **дёшево** (hex обрабатывать в python-скрипте, в контекст — только вывод) | ⭐ по умолчанию для наблюдения экрана и наведения |
|
||
| **raw VRAM** (`read_vram`) | средняя | требует ручного декодера (тайлы/4bpp/буфер) — ненадёжно | дёшево | только для того, чего нет в отрисовке: back-buffer, палитра, дескрипторы тайлов |
|
||
| **screenshot** (PNG) | быстрее всего для всего экрана | «на глаз» | **дороже всего** (картинка целиком в контекст) | последнее средство, быстрый визуальный гештальт |
|
||
|
||
**Правило (предпочтение пользователя):** МИНИМИЗИРОВАТЬ скриншоты. По умолчанию —
|
||
scrpix; raw VRAM — для «закулисных» данных; скриншот — только если иначе никак.
|
||
|
||
**Двойная буферизация (важно при чтении VRAM напрямую):** в видеопамяти ДВА
|
||
экрана — один отображается, второй в это время прорисовывается; после готовности
|
||
буферы переключаются. Активный набор дескрипторов выбирается портом **rgmod**
|
||
(`print (ib@C9)`). Список UI рисуется в АКТИВНОМ буфере (+0x20000), а КУРСОР мыши —
|
||
в ДРУГОМ буфере. Рекомендация: ставить эмулятор на `pause` и дампить именно
|
||
отображаемый экран (по rgmod). Из-за этого raw-VRAM-диффы давали ложные коорд.
|
||
курсора → **для курсора предпочитай scrpix**.
|
||
|
||
**Поиск курсора сложен:** текст И курсор оба белые (пен `0xFFFF`=65535), цветом не
|
||
изолировать — только ДИФФОМ (чуть двинуть мышь, сравнить два scrpix-грэба;
|
||
изменившиеся пиксели = курсор). Точка клика = **крайний левый верхний пиксель**
|
||
курсора.
|
||
|
||
**НЕ РЕШЕНО надёжно:** замкнутое наведение мыши на конкретный мелкий пункт.
|
||
Дифф-поиск курсора флакки (пробное движение + тайминги/буфер теряют курсор),
|
||
масштаб мышь→пиксель плывёт. **Надёжный путь — навигация клавиатурой** (точные
|
||
одиночные `press_key`). Если мышь macOS не в зоне окна MAME — координаты могут
|
||
«замораживаться».
|
||
|
||
---
|
||
|
||
## 7. Что уже отлажено и работает
|
||
|
||
- ✅ Конвертация в плагин; брейки/шаги/инспекция РАБОТАЮТ в hard-stop
|
||
(`register_periodic`).
|
||
- ✅ Чтение банкируемой памяти (`lmem`), VRAM/палитры, портов (`print (ib@)`).
|
||
- ✅ Полное управление клавиатурой с точными одиночными нажатиями. Тест 1 пройден:
|
||
навигация в C:\ZX\, запуск sprinter.zx → меню ZX → TR-DOS → выполнен `LIST`.
|
||
- ✅ Защитное отпускание ввода на `pause` (release_all) и авто-отпускание по
|
||
номеру кадра (tick_held в register_periodic) — иначе typematic-шторм.
|
||
- ⚠️ Частично/НЕ решено: надёжное замкнутое наведение мыши на мелкий пункт через
|
||
VRAM/scrpix (см. §6). Клавиатура — рабочий обходной путь.
|
||
|
||
**Структура списка в Flex Navigator (C:\ZX\):** строки текста с шагом 8px; левая
|
||
панель — колонки ~x48–150 (col1) и ~x166–220 (col2 с pent_*/sprinter);
|
||
sprinter.zx ≈ строка 6 колонки 2. (Это эвристика из наблюдений, не «читы из RAM».)
|
||
|
||
---
|
||
|
||
## 8. Типовые рецепты
|
||
|
||
**Старт отладочной сессии:**
|
||
```
|
||
status() # running/stopped + PC
|
||
set_breakpoint("0x8000") # PC-брейк (логический адрес)
|
||
resume()
|
||
# ...брейк сработал, машина в стопе...
|
||
status() # state=stop, БЕЗ таймаута (это и есть фикс плагина)
|
||
read_registers(); step(); read_registers() # PC двигается, оставаясь в стопе
|
||
```
|
||
|
||
**Проверка банкирования:** прочитать байт из банкируемого региона → переключить
|
||
банк (программой или `write_memory` в порт пейджинга) → снова прочитать тот же
|
||
адрес: значение должно измениться (чтение идёт через живое адресное пространство).
|
||
|
||
**Посмотреть кусок экрана дёшево:** `scrpix(x,y,w,h)` → получить hex-пены →
|
||
обработать в python (искать пятно/край/дифф) → в контекст вернуть только вывод.
|
||
|
||
**Корректно остановить MAME:** `debugger_command("exit")` (НЕ kill).
|
||
|
||
---
|
||
|
||
## 9. Источник истины и расширение
|
||
|
||
- Полная проектная документация: `/Users/tolik/Documents/GitHub/mame/src/CLAUDE.md`.
|
||
- Логика моста (можно править — изменения живые из-за симлинка):
|
||
`/Users/tolik/Documents/GitHub/mame/plugins/mamebridge/init.lua`.
|
||
- Объявления MCP-инструментов: `/Users/tolik/Documents/GitHub/mame/src/mame_mcp.py`.
|
||
- Эталон стиля плагина MAME: `/Users/tolik/Documents/GitHub/mame/plugins/gdbstub/`.
|
||
|
||
---
|
||
|
||
## 10. Авторитетный справочник API и команд (из docs.mamedev.org)
|
||
|
||
Сверено с официальной докой: plugins, luascript (ref-common/core/devices/mem/
|
||
input/debugger), debugger (general/execution/watchpoint…). Здесь — то, что
|
||
неочевидно или чем стоит пользоваться вместо строк `cmd`.
|
||
|
||
### 10.1 Lua-API дебаггера (нативный — чище, чем `cmd "..."`)
|
||
`machine.debugger` (manager.machine.debugger; `nil` если без `-debug`):
|
||
- `.execution_state` (rw) — `"run"` / `"stop"`. `.visible_cpu` (rw).
|
||
- `.consolelog[]`, `.errorlog[]` (ro) — строки вывода консоли/ошибок (можно ЧИТАТЬ
|
||
результат команд, а не только слать их!).
|
||
- `:command(str)` — выполнить консольную команду дебаггера.
|
||
|
||
`device.debug` (напр. `manager.machine.devices[":maincpu"].debug`):
|
||
- `:bpset(addr [,cond] [,act])` → номер bp; `:bpclear([n])`, `:bpenable([n])`,
|
||
`:bpdisable([n])`, `:bplist()` → таблица (поля bp: `.index .enabled .address
|
||
.condition .action`).
|
||
- `:wpset(space, type, addr, len [,cond] [,act])` — type `"r"|"w"|"rw"`, `space` —
|
||
объект адресного пространства (см. 10.4); `:wpclear([n])`, `:wplist(space)`
|
||
(поля wp: `.index .enabled .type .address .length .condition .action`).
|
||
- `:step([cnt])`, `:go()` (нативных `over`/`out`/disassemble НЕТ — они остаются на
|
||
`cmd`). `bpset`/`wpset` в биндинге требуют `cond` и `act` как `char*` → передавай
|
||
`""` если не нужны.
|
||
> ПРИМЕНЕНО В МОСТЕ: `bp`/`bpclr`/`bplist`/`wp`/`wpclr`/`step` идут через нативный
|
||
> `cpu().debug:*` (структурные возвраты: числовой индекс, таблицы; без скрейпинга
|
||
> consolelog). `over`/`out`/`dasm` — по-прежнему `run_cmd` (нет нативных).
|
||
> ВАЖНО: нативный `wpset` берёт адрес ЛИТЕРАЛЬНО в выбранном пространстве (без
|
||
> трансляции банков) — для Z80-окна в program давай 0x10000+ (напр. 0x18000 =
|
||
> окно 2), как в `write_memory`/`mem`. (Старый консольный `wpset 0x8000`
|
||
> неожиданно релоцировал в 0x18000; нативный — литеральный и предсказуемый.)
|
||
|
||
### 10.2 Команды дебаггера — авторитетный синтаксис
|
||
- Watchpoints: `wp[{d|i|o}][set] <addr>[:<space>],<len>,<type>[,<cond>[,<act>]]`.
|
||
`wpset`=program, `wpdset`=data, **`wpiset`=io**, `wposet`=opcodes. Можно и через
|
||
суффикс пространства: `wpset 0xC9:io,1,w`. Переменные в cond/act: `wpaddr`,
|
||
`wpdata`. Ещё: `wpclear/wpdisable/wpenable [n,…]`, `wplist [cpu]`.
|
||
- Breakpoints: `bpset <addr>[,<cond>[,<act>]]`, `bpclear/bpdisable/bpenable [n,…]`,
|
||
`bplist`.
|
||
- Шаги/выполнение: `s[tep] [n]`, `o[ver] [n]`, `out`, `g[o] [addr]`,
|
||
`gv[blank]`, `gi[nt] [irqline]`, `gt[ime] <ms>`, `ge[x] [exc,[cond]]`,
|
||
`gbt [cond]` / `gbf [cond]` (взятая/невзятая ветвь), `n[ext]` (до смены CPU),
|
||
`focus/ignore/observe <cpu>`, `trace {file|off}[,cpu[,…]]`, `traceover`,
|
||
`traceflush`.
|
||
- Общие: `print <expr>[,…]` (hex), `printf <fmt>[,arg…]` (%d %x %X %o %c %s %%),
|
||
`logerror`, `history [cpu[,len]]`, `softreset`, `hardreset`, `help [topic]`.
|
||
|
||
### 10.3 Выражения и операторы доступа к памяти
|
||
Форма: `[prefix]<size>[@|!]<addr>`.
|
||
- size: `b`=8, `w`=16, `d`=32, `q`=64.
|
||
- `@` = ПОДАВИТЬ side-effects (безопасное чтение), `!` = НЕ подавлять.
|
||
- prefix пространства: `p`/`lp`=program(лог.), `d`/`ld`=data, `i`/`li`=io,
|
||
`3`/`l3`=opcodes; физические — `pp pd pi p3`; `r`=прямой указатель program,
|
||
`o`=прямой opcodes, `m`=region, `s`=share.
|
||
- Примеры: `b@1234` (program byte), **`ib@C9`** (io byte, как мы читаем rgmod),
|
||
`dw@300` (data word), `pp b@addr` (физ. program byte).
|
||
- Встроенные функции: `min/max(a,b)`, `if(cond,t,f)`, `abs(x)`, `bit(x,n[,w])`,
|
||
`s8/s16/s32(x)` (знаковое расширение).
|
||
- Адресация устройства/пространства: `[tag][:space]` или `cpunum[:space]`
|
||
(напр. `maincpu`, `^melodypsg`, `.:adc`).
|
||
|
||
### 10.4 Память (Lua)
|
||
- Пространство: `dev.spaces[name]` (напр. `cpu.spaces["program"]`/`"io"`/
|
||
`"opcodes"`). Чтение: `:read_u8/16/32/64(addr)` (+ `_i` знаковые), логические
|
||
`:readv_*`, прямые `:read_direct_*`. **`:read_range(start,end,width[,step])` →
|
||
бинарная строка** (быстрый пакетный дамп — полезно для mem/vram). Запись:
|
||
`:write_u8/…(addr,val)`, `:writev_*`. Свойства: `.name .data_width
|
||
.address_mask .endianness`.
|
||
- Share/region: `machine.memory.shares[tag]` / `.regions[tag]` / `.banks[tag]`
|
||
(или `device:memshare(tag)`, `device:memregion(tag)`); `:read/write_u8/…`,
|
||
`.length .endianness .bitwidth`.
|
||
- ПРИМЕНЕНО В МОСТЕ: `mem`/`lmem`/`vram`/`share` читают через ОДИН
|
||
`obj:read_range(addr,addr+len-1,8)` (вместо `len` вызовов `read_u8` с pcall) +
|
||
быстрый hex по lookup-таблице. Фолбэк на побайтовый цикл, если у объекта нет
|
||
`read_range` (например, у share). Проверено: байт-в-байт совпадает с прежним.
|
||
|
||
### 10.5 Lifecycle / события (emu) — ВАЖНЫЙ нюанс
|
||
- Документированы: `emu.add_machine_{reset,stop,pause,resume,frame,pre_save,
|
||
post_load}_notifier(cb)` → объект подписки с `:unsubscribe()` и `.is_active`;
|
||
корутинные `emu.wait(sec)`, `emu.wait_next_frame()`, `emu.wait_next_update()`;
|
||
логи `emu.print_{error,warning,info,verbose,debug}`; `emu.subst_env`.
|
||
- **`emu.register_periodic(fn)` в доке ОТСУТСТВУЕТ**, но реален (luaengine.cpp:933)
|
||
и КРИТИЧЕН: он качается из `emulator_info::periodic_check()` в цикле
|
||
`while(is_stopped())` ДО `wait_for_debugger`, поэтому работает в hard-stop при
|
||
любом OSD-дебаггере. `add_machine_frame_notifier` в стопе МОЛЧИТ. → для pump
|
||
IPC/опроса в дебаггере используй `register_periodic`, а не frame-нотифаер.
|
||
|
||
### 10.6 Плагин и манифест
|
||
- `plugin.json`: `{ "plugin": { "name", "description", "version", "author",
|
||
"type": "plugin"|"library", "start": "false" } }`. Грузится `-plugin <name>`;
|
||
путь поиска `-pluginspath`; `start:false` → только по явному запросу.
|
||
- `init.lua`: таблица `exports` с `name/version/description/license/author` и
|
||
функцией `exports.startplugin()`; в конце `return exports`. Runtime-объект:
|
||
`manager.plugins[name]` (`.name .description .type .directory .start`).
|
||
- Эталоны: `plugins/gdbstub/` и наш `plugins/mamebridge/`.
|
||
- Встроенные плагины MAME: autofire, console, data, discord, dummy, gdbstub,
|
||
hiscore, inputmacro, layout, offscreen reload, timecode, timer.
|
||
|
||
Язык общения с пользователем: **русский**.
|