Vibe changes over upstream MAME (squashed)
Some checks failed
CI (macOS) / build-macos (push) Waiting to run
CI (Windows) / build-windows (gcc, gcc-x64, g++, mame, UCRT64, windows-latest, mingw-w64-ucrt-x86_64, mame) (push) Waiting to run
Check #include guards / validate (push) Has been cancelled

Содержит правки из следующих коммитов:
  - 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
This commit is contained in:
Tolik 2026-05-26 22:09:47 +10:00
parent 2fe5feb1ff
commit 6b6a5b0f9c
47 changed files with 4790 additions and 341 deletions

170
DEBUGGER_KEYMAP_HANDOFF.md Normal file
View File

@ -0,0 +1,170 @@
# Handoff: переназначаемые горячие клавиши в дебаггерах MAME
> Дай этот файл Claude в новой сессии (на другом компьютере) — он восстановит весь
> контекст задачи и сможет продолжить. Рабочая копия: `/Users/tolik/Documents/GitHub/mame`,
> ветка `Vibe`. Пользователь общается по-русски.
## Цель
Добавить в дебаггеры MAME возможность переназначать горячие клавиши с сохранением
между сессиями и UI для редактирования. Изначально просили для macOS-Cocoa-дебаггера
(`osd/modules/debugger/osx/`), затем — «для остальных дебаггеров» в
`src/osd/modules/debugger/`.
## Статус по дебаггерам
- **Cocoa (osx)** — ✅ СДЕЛАНО и собиралось (отдельная реализация на Objective-C).
- **ImGui** (`debugimgui.cpp`) — ✅ СДЕЛАНО, компилируется чисто.
- **Qt** (`debugqt.cpp` + `qt/`) — ✅ СДЕЛАНО, компилируется чисто (с `USE_QTDEBUG=1`).
- **Win** (`win/`) — ⛔ ПРОПУЩЕНО по решению пользователя (нельзя собрать/проверить на macOS;
нативный Win32-диалог — высокий риск вслепую). Не трогали.
- **gdbstub / none** — не GUI, не трогаем.
`-debugger auto` на macOS выбирает Cocoa (регистрируется первым, всегда инициализируется).
Для проверки других: `-debugger imgui -video bgfx` или `-debugger qt`.
---
## Архитектура
### Общий C++-модуль (для ImGui/Qt/Win) — НОВЫЙ
- `src/osd/modules/debugger/debugkeyconfig.h`
- `src/osd/modules/debugger/debugkeyconfig.cpp`
`namespace osd::debugger`:
- `struct key_shortcut { std::string key; bool ctrl, alt, shift; }`
`to_string()` → "Ctrl+Shift+F9" (переносимый текст), `from_string()`.
- `struct key_action { std::string id, label, group; key_shortcut default_shortcut; }`.
- `class keymap_config(std::vector<key_action>&& actions)` — хранит таблицу + overrideы:
`shortcut(id)`, `is_default`, `conflicting_action`, `set_shortcut/clear/reset/reset_all`,
`save(node)` / `load(node)` (узел `<keymap>` с дочерними `<key>`).
Каждый дебаггер задаёт СВОЮ таблицу действий (id/label/group/дефолты), а хранилище,
парсинг и XML — общие. Формат текстовый и единый → раскладка в `default.cfg` понятна
всем трём (Qt↔ImGui↔Win шарят). **Cocoa хранит в своём формате (codepoint+mask), с ним
не пересекается — это ОК (разные id, конфликтов в файле нет).**
### XML-константы (общие) — в `xmlconfig.h/.cpp`
Добавлены: `NODE_KEYMAP="keymap"`, `NODE_KEYMAP_ITEM="key"`,
`ATTR_KEYMAP_ACTION="action"`, `ATTR_KEYMAP_KEY="char"`, `ATTR_KEYMAP_MODIFIERS="modifiers"`.
(У C++-дебаггеров modifiers зашиты в текст `ATTR_KEYMAP_KEY`; у Cocoa — отдельные int.)
### Персистентность
Все дебаггеры пишут раскладку как ГЛОБАЛЬНУЮ — в `default.cfg`, через
`config_register("debugger", …)` и обработку `config_type::DEFAULT` (а не `SYSTEM`,
который для позиций окон). Сохраняются только отличия от дефолтов; пустой `<keymap>` не пишется.
---
## Что и где изменено
### Cocoa (Objective-C, отдельная реализация — НЕ использует debugkeyconfig)
НОВЫЕ:
- `osx/debugkeymap.h` / `osx/debugkeymap.mm` — синглтон `MAMEDebugKeyMap` (реестр действий,
лукапы, `applyToMenuItem:forAction:`, `refreshMenu:`, save/restore XML). Хранит unichar+mask.
- `osx/debugkeymapviewer.h` / `osx/debugkeymapviewer.mm` — окно `MAMEKeyBindingsWindow`
(таблица + запись через локальный монитор NSEvent, конфликты, Reset All).
ИЗМЕНЕНЫ:
- `osx/debugwindowhandler.{h,mm}``addCommonActionItems:` берёт клавиши из реестра;
пункт «Customize Keys…»; `showKeyBindings:`; `keyMapChanged:` рефрешит главное меню и
popup-кнопки окна; подписка на `MAMEDebugKeyMapChangedNotification`.
- `osx/disassemblyview.mm`оба построителя меню (контекст + action-button) через реестр.
- `debugosx.mm``build_menus` (главное меню Debug/Run/Step) через реестр; пункт
«Customize Keys…»; `config_load/config_save` обрабатывают `DEFAULT` для глобальной раскладки.
- `osx/debugkeymap.h` включает `"../xmlconfig.h"` (важно: с `../`, файл в подкаталоге osx/).
Идентификаторы Cocoa — camelCase ("stepInto" и т.п.).
### ImGui — `src/osd/modules/debugger/debugimgui.cpp`
- Добавлены include `debugkeyconfig.h`, `util/xmlfile.h`.
- Статическая таблица `f_imgui_keys[]` (имя↔ImGuiKey), функции `imgui_key_for_name`,
`shortcut_pressed(key_shortcut)`, `capture_shortcut(out)`.
- id-константы `IMGUI_ACT_*` (snake_case) + `imgui_default_actions()`.
- Поля класса: `keymap_config m_keymap`, `bool m_keybind_open`,
`std::string m_keybind_recording, m_keybind_status`. Инициализация в конструкторе.
- `handle_events()`: блок «global keys» переписан на `shortcut_pressed(m_keymap.shortcut(ID))`;
в начале `if(!m_keybind_recording.empty()) return;`.
- `init_debugger`: `config_register("debugger", …)`; **РАСШИРЕН `m_mapping`** — добавлены
F1/F2/F4, все буквы, 0-9, Space, Insert (иначе MAME не прокидывает эти клавиши в ImGui и
их нельзя ни нажать, ни записать).
- Меню Debug: ярлыки из `m_keymap.shortcut(ID).to_string()`, пункт «Customize keys...».
- Новые методы: `config_load`, `config_save`, `draw_keybindings()` (окно с таблицей,
запись клавиш, Esc/Delete, конфликты, Reset All). Вызов `draw_keybindings()` в `update()`.
### Qt — 3 файла (без новых файлов и без новых moc-классов; диалог инлайн на лямбдах)
- `qt/windowqt.h`: include `"../debugkeyconfig.h"`; в `DebuggerQt`
`virtual keymap_config &keymap()=0`, сигнал `keyBindingsChanged()`, `notifyKeyBindingsChanged()`;
в `WindowQt` — слоты `applyKeyBindings()`, `debugActCustomizeKeys()`, метод
`createKeyAction(id,text,slot)`, поле `std::map<std::string,QAction*> m_keyActions`.
Объявлена `std::vector<key_action> qtDefaultKeyActions();`.
- `qt/windowqt.cpp`: id-константы `ACT_*` + `qtDefaultKeyActions()`; действия создаются через
`createKeyAction` (ярлык из keymap, сохраняется в map); пункт «Customize Keys...»;
`applyKeyBindings()`; `debugActCustomizeKeys()` — инлайн `QDialog` с `QKeySequenceEdit`
на каждое действие, кнопки Ok/Cancel/RestoreDefaults; по OK пишет в keymap (через
`QKeySequence::toString(PortableText)`, берёт первый аккорд) и `notifyKeyBindingsChanged()`.
- `debugqt.cpp`: член `keymap_config m_keymap{qtDefaultKeyActions()}`, override `keymap()`,
в `configuration_load/save` добавлена ветка `config_type::DEFAULT``m_keymap.load/save`.
- **Дефолты Qt ИЗМЕНЕНЫ** со странных `F16F19` (заглушка в оригинале) на нормальные
F5/F6/F7/F8/F11/F10/Shift+F11/F3/Shift+F3/Ctrl+M,D,L,B/Ctrl+W/Ctrl+Q — единые с остальными.
### Build-скрипты
- `scripts/src/osd/modules.lua` — добавлены `debugkeyconfig.cpp/.h` в общий
`osdmodulesbuild()` (компилируется во ВСЕХ OSD).
- `scripts/src/osd/mac.lua`, `sdl.lua`, `sdl3.lua` — добавлены 4 osx-файла Cocoa
(`debugkeymap.{h,mm}`, `debugkeymapviewer.{h,mm}`).
- ImGui/Qt новых файлов не добавляли — правки в существующих.
---
## Сборка и проверка (то, что использовалось)
Окружение: macOS, OSD `sdl3`, конфиг `release64`, тулчейн `gmake-osx-clang`, Qt5 (`/usr/local/bin/qmake`).
Регенерация проекта (ОБЯЗАТЕЛЬНО с `--USE_QTDEBUG=1`, иначе qt-файлы выпадают из сборки):
```
cd /Users/tolik/Documents/GitHub/mame
3rdparty/genie/bin/darwin/genie --with-emulator --USE_QTDEBUG=1 --OPTIMIZE=3 \
--target='mame' --subtarget='mame' --build-dir='build' --osd='sdl3' \
--targetos='macosx' --PLATFORM='x86' --gcc=osx-clang --gcc_version=17.0.0 gmake
```
Сборка только нужных библиотек (быстро, без линковки всего MAME):
```
make -R --no-print-directory -C build/projects/sdl3/mame/gmake-osx-clang config=release64 osd_sdl3 qtdbg_sdl3
```
- `osd_sdl3` содержит `debugimgui.o`, `debugkeyconfig.o`, и osx-файлы Cocoa.
- `qtdbg_sdl3` содержит `debugqt.o`, `windowqt.o` + moc.
Статус последней сборки: **всё компилируется чисто, без ошибок/предупреждений** (по теме).
Полный бинарник (`mame`) НЕ релинковали — релинк потянул бы пересборку всего MAME.
Для живого теста нужен полный билд (напр. `make macosx_arm64_clang` или скрипт пользователя),
затем `~/Documents/MAME/debug.sh` (там `-debugger auto` → Cocoa). Для ImGui/Qt — сменить
`-debugger` (imgui требует `-video bgfx`).
Гочи сборки:
- genie без `--USE_QTDEBUG=1` отключает qt → `windowqt.o` пропадает из всех `.make`
(был эпизод: казалось, что файл не пересобирается).
- Объекты лежат в `build/osx_clang/obj/x64/Release/<lib>/...`.
- `qmake -query QT_INSTALL_LIBS` даёт `-F` для фреймворков (macOS).
---
## Что осталось / возможные продолжения
1. **Win-дебаггер** (пропущен). Если делать: ключи в `win/debugwininfo.cpp`
(`handle_key`, `WM_KEYDOWN`-switch по `VK_*` + `GetAsyncKeyState`) и
`win/disasmbasewininfo.cpp`; текст меню в `create_standard_menubar()`
(строки вида `"Run\tF5"`); персист через `debugwin` config (`DEFAULT`); пункт
«Customize keys…» + Win32-диалог (самое рискованное вслепую). Можно переиспользовать
`debugkeyconfig`; нужен маппинг `key_shortcut`↔`VK_*`. Текстовый формат уже совместим —
Windows-юзер и так может настроить через `default.cfg`/Qt/ImGui, а Win подхватит.
2. **Полный релинк** и интерактивная проверка ImGui/Qt вживую (запись клавиш, сохранение в
`default.cfg`, перезапуск — раскладка восстановилась).
3. Возможная унификация Cocoa на тот же текстовый формат `debugkeyconfig` (сейчас отдельный) —
не обязательно.
## Действия (id → дефолт), для ориентира
ImGui (snake_case): new_disasm=Ctrl+D, new_memory=Ctrl+M, new_bpoints=Ctrl+B,
new_wpoints=Ctrl+W, new_log=Ctrl+L, run=F5, run_next_cpu=F6, run_next_int=F7,
run_vblank=F8, run_and_hide=F12, step_into=F11, step_over=F10, step_out=F9,
soft_reset=F3, hard_reset=Shift+F3.
Qt (snake_case): new_memory=Ctrl+M, new_disasm=Ctrl+D, new_log=Ctrl+L, new_points=Ctrl+B,
new_devices=(нет), run=F5, run_and_hide=F12, run_next_cpu=F6, run_next_int=F7,
run_vblank=F8, step_into=F11, step_over=F10, step_out=Shift+F11, soft_reset=F3,
hard_reset=Shift+F3, close_window=Ctrl+W, quit=Ctrl+Q.

450
MAME_MCP_GUIDE.md Normal file
View File

@ -0,0 +1,450 @@
# Управление эмулятором 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 через банки
(логический 0x00000xFFFF → 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 0x00000x3FFF → 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`: az, 09, enter, space, esc, tab, backspace,
up/down/left/right, home, end, pgup, pgdn, ins, del, f1f12, 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; левая
панель — колонки ~x48150 (col1) и ~x166220 (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.
Язык общения с пользователем: **русский**.

696
plugins/mamebridge/init.lua Normal file
View File

@ -0,0 +1,696 @@
-- license:BSD-3-Clause
-- mamebridge plugin
-- File-IPC bridge that exposes MAME's debugger to an external MCP server.
--
-- Ported from the -autoboot_script version (src/mame_bridge.lua). The only
-- behavioural change is the pump: this plugin uses emu.register_periodic(),
-- which is driven by emulator_info::periodic_check() and therefore keeps firing
-- *even while the machine is hard-stopped in the debugger* (see the
-- while (is_stopped()) loop in src/emu/debug/debugcpu.cpp). The old frame
-- notifier went silent when stopped, breaking stop -> step -> inspect loops.
--
-- Launch:
-- mame <system> -debug -plugin mamebridge
--
-- Protocol: the MCP server drops a request file <DIR>/req_<id>.txt containing
-- one command line; this plugin processes it on each periodic tick and writes
-- the reply to <DIR>/resp_<id>.txt . The file-IPC protocol is identical to the
-- autoboot script, so mame_mcp.py needs no changes.
--
-- Env vars:
-- MAME_MCP_DIR IPC directory (default /tmp/mame_mcp)
-- MAME_MCP_CPU CPU device tag (default :maincpu)
local exports = {
name = "mamebridge",
version = "0.0.1",
description = "File-IPC debugger bridge for the MAME MCP server",
license = "BSD-3-Clause",
author = { name = "Tolik" } }
local mamebridge = exports
function mamebridge.startplugin()
local DIR = os.getenv("MAME_MCP_DIR") or "/tmp/mame_mcp"
local CPUTAG = os.getenv("MAME_MCP_CPU") or ":maincpu"
local SPACE = "program"
-- Screenshots go to a temp dir that the OS clears on reboot (/tmp on macOS).
local SNAPDIR = os.getenv("MAME_MCP_SNAP_DIR") or "/tmp/mame_snap"
-- Last counter value per mouse axis tag, so movement is continuous between
-- commands (a relative axis reads deltas; resetting it jumps the cursor back).
local mouse_pos = {}
-- Injected input held across frames: field-object -> remaining frame count,
-- or the sentinel `true` to hold until an explicit release. A frame notifier
-- re-applies set_value(1) each frame (MAME re-polls inputs per frame) and
-- counts finite holds down. NOTE: input is only processed while the machine
-- is RUNNING; frame notifiers (and the natkeyboard timer) don't tick while
-- hard-stopped, so resume() before typing/pressing for the input to register.
local held = {}
-- Z80 (and 8080-ish) registers worth reporting. Missing ones are skipped.
local REGS = { "PC","SP","AF","BC","DE","HL","IX","IY",
"AF2","BC2","DE2","HL2","I","R","IM","IFF1","IFF2","WZ" }
os.execute('mkdir -p "' .. DIR .. '" 2>/dev/null')
local lfs_ok, lfs = pcall(require, "lfs")
------------------------------------------------------------------- helpers
local function read_file(p)
local f = io.open(p, "rb"); if not f then return nil end
local d = f:read("*a"); f:close(); return d
end
local function write_atomic(path, data)
local tmp = path .. ".tmp"
local f = io.open(tmp, "wb"); if not f then return false end
f:write(data); f:close()
os.rename(tmp, path)
return true
end
local function cpu() return manager.machine.devices[CPUTAG] end
local function dbg() return manager.machine.debugger end
local function space() return cpu().spaces[SPACE] end
-- Parse an address argument. Accepts "0x.." explicit hex, plain decimal, and
-- bare hex (e.g. "B9", "9A09") so callers don't have to prefix 0x. (The MCP
-- server passes addresses through verbatim, and bp/wp let MAME's debugger
-- parse them, but mem/setmem/dasm resolve the address here in Lua.)
local function parse_addr(s)
if not s then return nil end
if s:match("^0[xX]%x+$") then return tonumber(s) end -- explicit hex
if s:match("^%d+$") then return tonumber(s) end -- decimal
if s:match("^%x+$") then return tonumber(s, 16) end -- bare hex
return tonumber(s)
end
-- Run a debugger console command and return any new console output it produced.
local function run_cmd(cmdstr)
local d = dbg()
local before = #d.consolelog
d:command(cmdstr)
local lines = {}
for i = before + 1, #d.consolelog do lines[#lines + 1] = d.consolelog[i] end
return table.concat(lines, "\n")
end
-- Fast binary-string -> uppercase-hex via a precomputed 256-entry table.
local HEXBYTE = {}
for i = 0, 255 do HEXBYTE[i] = string.format("%02X", i) end
local function hexify(bin)
local t = {}
for i = 1, #bin do t[i] = HEXBYTE[bin:byte(i)] end
return table.concat(t)
end
-- Read `len` bytes from `obj` (an address space or a memory_share) starting at
-- `addr`, returned as a contiguous hex string. Fast path: ONE bulk
-- read_range(start, end, 8) call (per the Lua mem API) instead of `len`
-- per-byte read_u8 calls — a big win on large dumps (e.g. 4K VRAM reads).
-- Falls back to a byte loop if the object has no read_range or it misbehaves
-- (so memory_share, which may lack read_range, still works).
local function read_block(obj, addr, len)
if len > 4096 then len = 4096 end
local ok, bin = pcall(function() return obj:read_range(addr, addr + len - 1, 8) end)
if ok and type(bin) == "string" and #bin == len then return hexify(bin) end
local t = {}
for i = 0, len - 1 do
local ok2, b = pcall(function() return obj:read_u8(addr + i) end)
t[#t + 1] = HEXBYTE[(ok2 and b or 0) & 0xff]
end
return table.concat(t)
end
------------------------------------------------------------------- actions
local function do_regs()
local c, out = cpu(), {}
for _, r in ipairs(REGS) do
local ok, ent = pcall(function() return c.state[r] end)
if ok and ent then
local ok2, v = pcall(function() return ent.value end)
if ok2 and v then out[#out + 1] = string.format("%s=0x%X", r, v) end
end
end
return table.concat(out, " ")
end
local function do_mem(addr, len)
return read_block(space(), addr, len)
end
-- On Sprinter the Z80's 64K logical space is mapped into the CPU program
-- space at 0x10000 (windows: 0->0x10000, 1->0x14000, 2->0x18000, 3->0x1c000).
-- Reading program[L] also works once the FPGA bitstream is loaded, but goes
-- through bootstrap_r (returns fastram while loading), so read 0x10000|L for a
-- reliable view of what the CPU currently sees through the banks.
local function do_lmem(laddr, len)
return do_mem(0x10000 | (laddr & 0xffff), len)
end
-- Read a rectangle of RENDERED screen pixels (the machine's video output,
-- decoded from VRAM by the emulated hardware) via screen:pixel(x,y). Returns
-- one 4-hex pen index per pixel, row-major. This is the screen as displayed
-- (handles tile/4bpp/double-buffer for us), not raw VRAM bytes.
local function do_scrpix(x, y, w, h)
local scr
for _, s in pairs(manager.machine.screens) do scr = s; break end
if not scr then return "ERROR: no screen device" end
w = w or 1; h = h or 1
if w * h > 8192 then return "ERROR: area too big (max 8192 px)" end
local t = {}
for dy = 0, h - 1 do
for dx = 0, w - 1 do
local ok, p = pcall(function() return scr:pixel(x + dx, y + dy) end)
t[#t + 1] = string.format("%04X", ok and (p & 0xffff) or 0)
end
end
return table.concat(t)
end
-- Find a memory_share whose tag contains `name` (e.g. "vram", "fastram").
local function find_share(name)
for tag, sh in pairs(manager.machine.memory.shares) do
if tag:find(name, 1, true) then return sh, tag end
end
return nil
end
-- Read raw bytes from a memory_share (e.g. the 256K "vram"), bypassing Z80
-- banking entirely. Useful for inspecting screen data, palette, tile/mode
-- descriptors directly. Returns hex like do_mem.
local function do_share(name, addr, len)
local sh = find_share(name)
if not sh then return "ERROR: no share matching '" .. tostring(name) .. "'" end
return read_block(sh, addr, len)
end
-- List memory shares and regions (tags + sizes) for discovery.
local function do_shares()
local out = {}
for tag, sh in pairs(manager.machine.memory.shares) do
local ok, sz = pcall(function() return sh.size end)
out[#out + 1] = string.format("share %s size=%s", tag, ok and tostring(sz) or "?")
end
for tag, rg in pairs(manager.machine.memory.regions) do
local ok, sz = pcall(function() return rg.size end)
out[#out + 1] = string.format("region %s size=%s", tag, ok and tostring(sz) or "?")
end
return table.concat(out, "\n")
end
local function do_setmem(addr, hex)
local s, i = space(), 0
for byte in hex:gmatch("%x%x") do
s:write_u8(addr + i, tonumber(byte, 16)); i = i + 1
end
return "ok wrote " .. i .. " byte(s)"
end
local function do_status()
local st = dbg().execution_state
local pc = "?"
local ok, v = pcall(function() return cpu().state["PC"].value end)
if ok then pc = string.format("0x%X", v) end
return "state=" .. st .. " PC=" .. pc
end
local function do_dasm(addr, nbytes)
local tmp = DIR .. "/_dasm.tmp"
run_cmd(string.format("dasm %s,0x%X,%d", tmp, addr, nbytes))
local d = read_file(tmp) or "(no output)"
os.remove(tmp)
return d
end
-- Save a screenshot of the active screen to SNAPDIR and return its full path,
-- so the caller can open the PNG. With no name, a timestamped one is generated.
-- Note: the snapshot captures the last drawn frame; while hard-stopped the
-- image is frozen, so resume briefly before snapping for a fresh frame.
local snap_seq = 0
local function do_snap(name)
os.execute('mkdir -p "' .. SNAPDIR .. '" 2>/dev/null')
local fname
if name and name:match("^/") then
fname = name -- absolute path as given
elseif name then
fname = SNAPDIR .. "/" .. name -- bare name -> under SNAPDIR
else
snap_seq = snap_seq + 1
fname = string.format("%s/snap_%d_%03d.png", SNAPDIR, os.time(), snap_seq)
end
local scr
for _, s in pairs(manager.machine.screens) do scr = s; break end
if not scr then return "ERROR: no screen device" end
local err = scr:snapshot(fname) -- returns nil on success
if err ~= nil then return "ERROR: snapshot failed: " .. tostring(err) end
return fname
end
------------------------------------------------------------------- input
-- Current emulated frame number (from the first screen), for timing presses.
local function framenum()
for _, s in pairs(manager.machine.screens) do
local ok, n = pcall(function() return s:frame_number() end)
if ok then return n end
end
return 0
end
-- List input ports and their fields (tag + mask + name), for discovery.
local function do_ports()
local out = {}
for ptag, port in pairs(manager.machine.ioport.ports) do
local fields = {}
for fname, field in pairs(port.fields) do
local ok, m = pcall(function() return field.mask end)
fields[#fields + 1] = string.format(" 0x%04X %s", ok and m or 0, fname)
end
table.sort(fields)
out[#out + 1] = ptag .. "\n" .. table.concat(fields, "\n")
end
table.sort(out)
return table.concat(out, "\n")
end
-- Type text via the natural keyboard (uses each key's PORT_CHAR mapping).
-- Enable in_use so the keystrokes are actually delivered.
local function do_type(text)
if not text then return "ERROR: no text" end
local nk = manager.machine.natkeyboard
nk.in_use = true
nk:post(text)
return "posted " .. #text .. " char(s) (machine must be running to apply)"
end
-- Named keys mapped to a LIST of "porttag 0xMASK" specs (tag without leading
-- ":"). On the real host a keypress goes to BOTH keyboards at once, so each
-- key drives the PC PS/2 keyboard (kbd:ms_naturl:Px.y) AND, where it exists,
-- the ZX-Spectrum matrix (IO_LINEn). Masks taken from the live `ports` dump.
local KEYS = {
a={"kbd:ms_naturl:P1.6 0x04","IO_LINE1 0x01"},
b={"kbd:ms_naturl:P1.5 0x10","IO_LINE7 0x10"},
c={"kbd:ms_naturl:P1.4 0x08","IO_LINE0 0x08"},
d={"kbd:ms_naturl:P1.4 0x04","IO_LINE1 0x04"},
e={"kbd:ms_naturl:P1.4 0x20","IO_LINE2 0x04"},
f={"kbd:ms_naturl:P1.5 0x02","IO_LINE1 0x08"},
g={"kbd:ms_naturl:P1.5 0x04","IO_LINE1 0x10"},
h={"kbd:ms_naturl:P2.0 0x02","IO_LINE6 0x10"},
i={"kbd:ms_naturl:P1.4 0x01","IO_LINE5 0x04"},
j={"kbd:ms_naturl:P2.0 0x04","IO_LINE6 0x08"},
k={"kbd:ms_naturl:P1.4 0x02","IO_LINE6 0x04"},
l={"kbd:ms_naturl:P2.2 0x08","IO_LINE6 0x02"},
m={"kbd:ms_naturl:P2.0 0x10","IO_LINE7 0x04"},
n={"kbd:ms_naturl:P2.0 0x08","IO_LINE7 0x08"},
o={"kbd:ms_naturl:P2.2 0x02","IO_LINE5 0x02"},
p={"kbd:ms_naturl:P2.3 0x20","IO_LINE5 0x01"},
q={"kbd:ms_naturl:P1.6 0x02","IO_LINE2 0x01"},
r={"kbd:ms_naturl:P1.5 0x01","IO_LINE2 0x08"},
s={"kbd:ms_naturl:P1.2 0x08","IO_LINE1 0x02"},
t={"kbd:ms_naturl:P1.5 0x20","IO_LINE2 0x10"},
u={"kbd:ms_naturl:P2.0 0x20","IO_LINE5 0x08"},
v={"kbd:ms_naturl:P1.5 0x08","IO_LINE0 0x10"},
w={"kbd:ms_naturl:P1.2 0x04","IO_LINE2 0x02"},
x={"kbd:ms_naturl:P1.2 0x10","IO_LINE0 0x04"},
y={"kbd:ms_naturl:P2.0 0x01","IO_LINE5 0x10"},
z={"kbd:ms_naturl:P1.6 0x10","IO_LINE0 0x02"},
["0"]={"kbd:ms_naturl:P2.3 0x01","IO_LINE4 0x01"},
["1"]={"kbd:ms_naturl:P1.6 0x01","IO_LINE3 0x01"},
["2"]={"kbd:ms_naturl:P1.2 0x02","IO_LINE3 0x02"},
["3"]={"kbd:ms_naturl:P1.4 0x40","IO_LINE3 0x04"},
["4"]={"kbd:ms_naturl:P1.5 0x40","IO_LINE3 0x08"},
["5"]={"kbd:ms_naturl:P1.5 0x80","IO_LINE3 0x10"},
["6"]={"kbd:ms_naturl:P2.0 0x40","IO_LINE4 0x10"},
["7"]={"kbd:ms_naturl:P2.0 0x80","IO_LINE4 0x08"},
["8"]={"kbd:ms_naturl:P1.4 0x80","IO_LINE4 0x04"},
["9"]={"kbd:ms_naturl:P2.2 0x01","IO_LINE4 0x02"},
enter={"kbd:ms_naturl:P2.1 0x10","IO_LINE6 0x01"},
space={"kbd:ms_naturl:P2.4 0x80","IO_LINE7 0x01"},
-- ZX cursor keys live on the 5/6/7/8 matrix positions
up={"kbd:ms_naturl:P2.4 0x01","IO_LINE4 0x08"},
down={"kbd:ms_naturl:P2.3 0x10","IO_LINE4 0x10"},
left={"kbd:ms_naturl:P2.1 0x08","IO_LINE3 0x10"},
right={"kbd:ms_naturl:P2.4 0x40","IO_LINE4 0x04"},
shift={"kbd:ms_naturl:P1.7 0x02","IO_LINE0 0x01"}, -- ZX CAPS SHIFT
symbolshift={"IO_LINE7 0x02"},
-- PC-only keys (no ZX matrix equivalent)
esc={"kbd:ms_naturl:P1.6 0x40"}, tab={"kbd:ms_naturl:P1.6 0x20"},
backspace={"kbd:ms_naturl:P2.1 0x20"}, bs={"kbd:ms_naturl:P2.1 0x20"},
home={"kbd:ms_naturl:P2.5 0x01"}, ["end"]={"kbd:ms_naturl:P2.5 0x20"},
pgup={"kbd:ms_naturl:P1.2 0x01"}, pgdn={"kbd:ms_naturl:P1.2 0x20"},
ins={"kbd:ms_naturl:P2.6 0x01"}, del={"kbd:ms_naturl:P2.6 0x02"},
f1={"kbd:ms_naturl:P1.2 0x40"}, f2={"kbd:ms_naturl:P1.2 0x80"},
f3={"kbd:ms_naturl:P2.6 0x40"}, f4={"kbd:ms_naturl:P2.6 0x80"},
f5={"kbd:ms_naturl:P2.1 0x40"}, f6={"kbd:ms_naturl:P2.1 0x80"},
f7={"kbd:ms_naturl:P2.2 0x40"}, f8={"kbd:ms_naturl:P2.2 0x80"},
f9={"kbd:ms_naturl:P2.3 0x40"}, f10={"kbd:ms_naturl:P2.3 0x80"},
f11={"kbd:ms_naturl:P2.5 0x40"}, f12={"kbd:ms_naturl:P2.5 0x80"},
lshift={"kbd:ms_naturl:P1.7 0x02"}, rshift={"kbd:ms_naturl:P1.7 0x10"},
ctrl={"kbd:ms_naturl:P1.1 0x02"}, lctrl={"kbd:ms_naturl:P1.1 0x02"},
rctrl={"kbd:ms_naturl:P1.1 0x10"}, alt={"kbd:ms_naturl:P1.3 0x04"},
lalt={"kbd:ms_naturl:P1.3 0x04"}, ralt={"kbd:ms_naturl:P1.3 0x08"},
capslock={"kbd:ms_naturl:P1.1 0x20"},
[";"]={"kbd:ms_naturl:P2.3 0x02"}, ["'"]={"kbd:ms_naturl:P2.3 0x04"},
["/"]={"kbd:ms_naturl:P2.3 0x08"}, ["."]={"kbd:ms_naturl:P2.2 0x10"},
[","]={"kbd:ms_naturl:P1.4 0x10"}, ["-"]={"kbd:ms_naturl:P2.2 0x20"},
["="]={"kbd:ms_naturl:P2.1 0x01"}, ["["]={"kbd:ms_naturl:P2.2 0x04"},
["]"]={"kbd:ms_naturl:P2.1 0x02"}, ["\\"]={"kbd:ms_naturl:P2.1 0x04"},
["`"]={"kbd:ms_naturl:P1.6 0x80"},
}
-- Type text honouring {KEY} codes, e.g. "dir{ENTER}" or "{F3}".
local function do_typecode(text)
if not text then return "ERROR: no text" end
manager.machine.natkeyboard:post_coded(text)
return "posted coded (machine must be running to apply)"
end
local function do_kbstat()
local nk = manager.machine.natkeyboard
local out = { string.format("empty=%s is_posting=%s can_post=%s in_use=%s",
tostring(nk.empty), tostring(nk.is_posting), tostring(nk.can_post), tostring(nk.in_use)) }
for _, kb in pairs(nk.keyboards) do
out[#out + 1] = string.format(" kbd %s [%s] enabled=%s",
kb.tag, kb.name, tostring(kb.enabled))
end
return table.concat(out, "\n")
end
-- Choose which keyboard(s) natkeyboard posts into: enable those whose tag
-- contains `substr` (e.g. "ms_naturl" for the PC PS/2 keyboard, "IO_LINE" for
-- the ZX matrix), disable the rest. "all" enables everything. This is what
-- makes `type`/`typecode` land in the keyboard the running program reads, and
-- natkeyboard sends one scan-code per key (correct timing, no SIO repeat storm).
local function do_kbsel(substr)
local nk = manager.machine.natkeyboard
nk.in_use = true
local out = {}
for _, kb in pairs(nk.keyboards) do
local on = (not substr) or (substr == "all") or (kb.tag:find(substr, 1, true) ~= nil)
kb.enabled = on
if on then out[#out + 1] = "enabled " .. kb.tag end
end
return #out > 0 and table.concat(out, "\n") or "no keyboard matched '" .. tostring(substr) .. "'"
end
-- Resolve a field by port tag + bit mask (avoids parsing names with spaces).
local function find_field(ptag, mask)
local port = manager.machine.ioport.ports[ptag]
if not port then return nil, "no port '" .. tostring(ptag) .. "'" end
for _, f in pairs(port.fields) do
local ok, m = pcall(function() return f.mask end)
if ok and m == mask then return f end
end
return nil, string.format("no field with mask 0x%X in '%s'", mask or -1, ptag)
end
-- Press a digital field for `frames` emulated frames, then auto-release.
-- held[f] = { rel = <frame at/after which to release> }.
local function do_press(ptag, mask, frames)
local f, err = find_field(ptag, mask)
if not f then return "ERROR: " .. err end
frames = frames or 2
held[f] = { rel = framenum() + frames }
f:set_value(1)
return string.format("pressing %s 0x%X for %d frame(s)", ptag, mask, frames)
end
-- Hold a field down until an explicit release. held[f] = { hold = true }.
local function do_hold(ptag, mask)
local f, err = find_field(ptag, mask)
if not f then return "ERROR: " .. err end
held[f] = { hold = true }
f:set_value(1)
return "holding " .. ptag .. string.format(" 0x%X (call release)", mask)
end
local function do_release(ptag, mask)
local f, err = find_field(ptag, mask)
if not f then return "ERROR: " .. err end
held[f] = nil
f:clear_value()
return "released"
end
-- Release every held input. Called on pause so stopping the machine can't
-- leave a key/button stuck (auto-release only ticks while running).
local function release_all()
local n = 0
for f in pairs(held) do
pcall(function() f:clear_value() end)
held[f] = nil
n = n + 1
end
return n
end
-- Set an analog field (e.g. mouse axis) to a value, held for `frames` frames
-- (re-applied each frame so relative axes like the mouse accumulate motion).
local function do_analog(ptag, mask, value, frames)
local f, err = find_field(ptag, mask)
if not f then return "ERROR: " .. err end
if frames and frames > 0 then
held[f] = { v = value, rel = framenum() + frames }
end
f:set_value(value)
return string.format("set %s 0x%X = %s (%s frame(s))", ptag, mask, tostring(value),
frames and tostring(frames) or "1")
end
-- Press a named key for `frames` frames on BOTH keyboards it maps to.
local function do_key(name, frames)
if not name then return "ERROR: no key name" end
local spec = KEYS[name] or KEYS[name:lower()]
if not spec then return "ERROR: unknown key '" .. name .. "'" end
frames = frames or 3
local out = {}
for _, s in ipairs(spec) do
local p, m = s:match("^(%S+)%s+(%S+)$")
out[#out + 1] = do_press(":" .. p, tonumber(m), frames)
end
return table.concat(out, "; ")
end
-- Move BOTH mice (Kempston :mouse_input1/2 mask 0xFF, and RS232 COM mouse
-- :rs232:microsoft_mouse:X/Y mask 0xFFF) by relative dx,dy over `frames`
-- frames, matching how host motion drives both at once.
-- Start a per-frame counter on an axis (step per frame for `frames` frames).
local function move_axis(tag, mask, step, frames)
local f = find_field(tag, mask)
if not f then return false end
held[f] = { mv = mouse_pos[tag] or 0, step = step, mask = mask,
rel = framenum() + frames, tag = tag }
return true
end
-- Move BOTH mice by stepping their axes each frame. Flex Navigator reads the
-- COM (serial) mouse; the Kempston counter is driven too for ZX software.
-- dx/dy are the per-frame step (signed); total travel ~ step*frames*sensitivity.
local function do_mouse(dx, dy, frames)
frames = frames or 8
local out = {}
if dx and dx ~= 0 then
if move_axis(":rs232:microsoft_mouse:X", 0xFFF, dx, frames) then out[#out+1] = "COM.X" end
if move_axis(":mouse_input1", 0xFF, dx, frames) then out[#out+1] = "K.X" end
end
if dy and dy ~= 0 then
if move_axis(":rs232:microsoft_mouse:Y", 0xFFF, dy, frames) then out[#out+1] = "COM.Y" end
if move_axis(":mouse_input2", 0xFF, dy, frames) then out[#out+1] = "K.Y" end
end
return #out > 0
and string.format("mouse move %s step(%s,%s) x%d frames", table.concat(out, ","), tostring(dx), tostring(dy), frames)
or "no motion"
end
-- Click a mouse button on BOTH mice. btn: left|right|middle.
local MBTN = {
left = {"mouse_input3 0x01", "rs232:microsoft_mouse:BTN 0x02"},
right = {"mouse_input3 0x02", "rs232:microsoft_mouse:BTN 0x01"},
middle = {"mouse_input3 0x04"},
}
local function do_mclick(btn, frames)
local spec = MBTN[(btn or "left"):lower()]
if not spec then return "ERROR: button must be left|right|middle" end
frames = frames or 3
local out = {}
for _, s in ipairs(spec) do
local p, m = s:match("^(%S+)%s+(%S+)$")
out[#out + 1] = do_press(":" .. p, tonumber(m), frames)
end
return table.concat(out, "; ")
end
------------------------------------------------------- native debugger ops
-- Use the device_debug Lua interface (cpu().debug:bpset/wpset/step/...) instead
-- of console-command strings: structured returns (numeric indices, tables), no
-- console-log scraping. NOTE: over/out have no native binding, and there is no
-- native disassemble, so those stay as console commands (run_cmd).
local function ddbg() return cpu().debug end
-- bpset(addr, cond, act) -> index. cond/act are required char* args in the
-- binding, so pass "" when unused (empty = unconditional).
local function do_bp(addr, cond)
local n = ddbg():bpset(addr, cond or "", "")
return "Breakpoint " .. n .. " set"
end
local function do_bpclr(n)
if n == nil then ddbg():bpclear(); return "Cleared all breakpoints" end
return ddbg():bpclear(tonumber(n)) and ("Breakpoint " .. n .. " cleared")
or ("ERROR: no breakpoint " .. tostring(n))
end
-- bplist() -> table keyed by index; values expose index/address/condition/
-- action/enabled. Format sorted by index.
local function do_bplist()
local bps, keys = ddbg():bplist(), {}
for idx in pairs(bps) do keys[#keys + 1] = idx end
table.sort(keys)
local out = {}
for _, idx in ipairs(keys) do
local bp = bps[idx]
out[#out + 1] = string.format("%d @ %04X%s%s", bp.index, bp.address,
(bp.condition ~= "" and bp.condition ~= "1") and (" if " .. bp.condition) or "",
bp.enabled and "" or " [disabled]")
end
return #out > 0 and table.concat(out, "\n") or "No breakpoints"
end
-- wpset(space, type, addr, len, cond, act) -> index. `space` is an address-space
-- object from cpu().spaces[...]. The address is LITERAL in that space (no bank
-- translation), so for a Z80 window in program space pass the 0x10000+ address
-- (consistent with write_memory / mem). spacename: program|data|io|opcodes.
local function do_wp(addr, len, wtype, spacename)
spacename = (spacename or "program"):lower()
local sp = cpu().spaces[spacename]
if not sp then return "ERROR: no '" .. spacename .. "' address space" end
local n = ddbg():wpset(sp, wtype, addr, len, "", "")
return string.format("Watchpoint %d set (%s)", n, spacename)
end
local function do_wpclr(n)
if n == nil then ddbg():wpclear(); return "Cleared all watchpoints" end
return ddbg():wpclear(tonumber(n)) and ("Watchpoint " .. n .. " cleared")
or ("ERROR: no watchpoint " .. tostring(n))
end
-- Single-step n instructions (native single_step). The step executes once this
-- periodic callback returns and the debugger's stop loop resumes, so don't read
-- PC here (it would be stale) — the caller polls status/regs afterwards.
local function do_step(n)
ddbg():step(n or 1)
return "step " .. (n or 1)
end
----------------------------------------------------------------- dispatch
local function dispatch(line)
if not manager.machine.debugger then
return "ERROR: debugger not enabled (start MAME with -debug)"
end
local words = {}
for w in line:gmatch("%S+") do words[#words + 1] = w end
local verb = words[1]
local ok, res = pcall(function()
if verb == "regs" then return do_regs()
elseif verb == "mem" then return do_mem(parse_addr(words[2]), tonumber(words[3]))
elseif verb == "lmem" then return do_lmem(parse_addr(words[2]), tonumber(words[3]))
elseif verb == "vram" then return do_share("vram", parse_addr(words[2]), tonumber(words[3]))
elseif verb == "scrpix" then return do_scrpix(tonumber(words[2]), tonumber(words[3]), tonumber(words[4] or "1"), tonumber(words[5] or "1"))
elseif verb == "share" then return do_share(words[2], parse_addr(words[3]), tonumber(words[4]))
elseif verb == "shares" then return do_shares()
elseif verb == "setmem" then return do_setmem(parse_addr(words[2]), words[3])
elseif verb == "bp" then
local cond = line:match("^%S+%s+%S+%s+(.+)$")
return do_bp(parse_addr(words[2]), cond)
elseif verb == "bpclr" then return do_bpclr(words[2])
elseif verb == "bplist" then return do_bplist()
elseif verb == "wp" then
-- wp ADDR LEN ACCESS [space] space: program(default)|data|io|opcodes.
return do_wp(parse_addr(words[2]), tonumber(words[3]), words[4], words[5])
elseif verb == "wpclr" then return do_wpclr(words[2])
elseif verb == "step" then return do_step(tonumber(words[2] or "1"))
elseif verb == "over" then return run_cmd("over " .. (words[2] or "1"))
elseif verb == "out" then return run_cmd("out")
elseif verb == "cont" then dbg().execution_state = "run"; return "running"
elseif verb == "pause" then local n = release_all(); dbg().execution_state = "stop"; return "stopped (released " .. n .. " input(s))"
elseif verb == "status" then return do_status()
elseif verb == "dasm" then return do_dasm(parse_addr(words[2]), tonumber(words[3] or "32"))
elseif verb == "snap" then return do_snap(line:match("^snap%s+(.+)$"))
elseif verb == "ports" then return do_ports()
elseif verb == "type" then return do_type(line:match("^type%s+(.+)$"))
elseif verb == "typecode" then return do_typecode(line:match("^typecode%s+(.+)$"))
elseif verb == "kbstat" then return do_kbstat()
elseif verb == "kbsel" then return do_kbsel(words[2])
elseif verb == "press" then return do_press(words[2], parse_addr(words[3]), tonumber(words[4] or "2"))
elseif verb == "key" then return do_key(words[2], tonumber(words[3] or "3"))
elseif verb == "mouse" then return do_mouse(tonumber(words[2] or "0"), tonumber(words[3] or "0"), tonumber(words[4] or "3"))
elseif verb == "mclick" then return do_mclick(words[2], tonumber(words[3] or "3"))
elseif verb == "hold" then return do_hold(words[2], parse_addr(words[3]))
elseif verb == "release" then return do_release(words[2], parse_addr(words[3]))
elseif verb == "analog" then return do_analog(words[2], parse_addr(words[3]), tonumber(words[4]), tonumber(words[5] or "0"))
elseif verb == "cmd" then return run_cmd(line:match("^cmd%s+(.+)$") or "")
else return "ERROR: unknown command '" .. tostring(verb) .. "'" end
end)
return ok and tostring(res) or ("ERROR: " .. tostring(res))
end
--------------------------------------------------------------------- pump
-- Auto-release / re-apply held inputs, timed by emulated frame number. Driven
-- from poll() (register_periodic) because add_machine_frame_notifier did NOT
-- tick here, leaving keys stuck and causing typematic-repeat storms. Only acts
-- while running (input needs the machine moving; pause releases everything).
local function tick_held()
if next(held) == nil then return end -- nothing held: skip (and avoid touching
-- the debugger before it exists at startup)
if dbg().execution_state ~= "run" then return end
local now = framenum()
for f, h in pairs(held) do
if h.step then
-- mouse counter: both the Kempston counter and the COM (serial HLE) mouse
-- move on a CHANGE of the axis value (constant override = no motion). So
-- advance the value each frame; successive reads differ by `step` =>
-- continuous movement. mask is 0xFF (Kempston) or 0xFFF (COM).
h.mv = (h.mv + h.step) & (h.mask or 0xFF)
f:set_value(h.mv)
mouse_pos[h.tag] = h.mv
-- When done, STOP advancing but DON'T clear: leaving the value frozen
-- means delta 0 (cursor holds) instead of snapping back to default.
if now >= h.rel then held[f] = nil end
elseif h.hold then
f:set_value(h.v or 1)
elseif now >= h.rel then
f:clear_value(); held[f] = nil
else
f:set_value(h.v or 1)
end
end
end
local function poll()
tick_held()
if not lfs_ok then return end
for entry in lfs.dir(DIR) do
local id = entry:match("^req_(%d+)%.txt$")
if id then
local reqp = DIR .. "/" .. entry
local data = read_file(reqp)
if data then
local reply = dispatch(data)
write_atomic(DIR .. "/resp_" .. id .. ".txt", reply)
os.remove(reqp)
end
end
end
end
-- Fires from emulator_info::periodic_check(), so it keeps servicing requests
-- (and ticking held inputs) both while running and while hard-stopped.
emu.register_periodic(poll)
emu.print_info("mamebridge plugin active. IPC dir = " .. DIR)
end
return exports

View File

@ -0,0 +1,10 @@
{
"plugin": {
"name": "mamebridge",
"description": "File-IPC debugger bridge for the MAME MCP server",
"version": "0.0.1",
"author": "Tolik",
"type": "plugin",
"start": "false"
}
}

View File

@ -26,6 +26,8 @@ exports.author = { name = "Carl" }
local portname = exports local portname = exports
local reset_subscription
function portname.startplugin() function portname.startplugin()
local json = require("json") local json = require("json")
local ctrlrpath = manager.options.entries.ctrlrpath:value():match("([^;]+)") local ctrlrpath = manager.options.entries.ctrlrpath:value():match("([^;]+)")
@ -70,7 +72,9 @@ function portname.startplugin()
end end
end end
emu.register_start(function() -- emu.register_start was removed from the Lua API; the machine-reset notifier
-- is the current equivalent (fires once the machine's ioports exist).
reset_subscription = emu.add_machine_reset_notifier(function()
local file = emu.file(ctrlrpath .. "/portname", "r") local file = emu.file(ctrlrpath .. "/portname", "r")
local ret = file:open(get_filename()) local ret = file:open(get_filename())
if ret then if ret then

View File

@ -86,6 +86,10 @@ project ("osd_" .. _OPTIONS["osd"])
MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm",

View File

@ -65,6 +65,8 @@ function osdmodulesbuild()
MAME_DIR .. "src/osd/interface/nethandler.h", MAME_DIR .. "src/osd/interface/nethandler.h",
MAME_DIR .. "src/osd/interface/uievents.h", MAME_DIR .. "src/osd/interface/uievents.h",
MAME_DIR .. "src/osd/modules/debugger/debug_module.h", MAME_DIR .. "src/osd/modules/debugger/debug_module.h",
MAME_DIR .. "src/osd/modules/debugger/debugkeyconfig.cpp",
MAME_DIR .. "src/osd/modules/debugger/debugkeyconfig.h",
MAME_DIR .. "src/osd/modules/debugger/debuggdbstub.cpp", MAME_DIR .. "src/osd/modules/debugger/debuggdbstub.cpp",
MAME_DIR .. "src/osd/modules/debugger/debugimgui.cpp", MAME_DIR .. "src/osd/modules/debugger/debugimgui.cpp",
MAME_DIR .. "src/osd/modules/debugger/debugwin.cpp", MAME_DIR .. "src/osd/modules/debugger/debugwin.cpp",

View File

@ -340,6 +340,10 @@ project ("osd_" .. _OPTIONS["osd"])
MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm",

View File

@ -367,6 +367,10 @@ project ("osd_" .. _OPTIONS["osd"])
MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugcommandhistory.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugconsole.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymap.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugkeymapviewer.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.mm",
MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugview.h",
MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm",

15
src/.claude/settings.json Normal file
View File

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(python3 -c \"import json; json.load\\(open\\('/Users/tolik/Documents/GitHub/mame/plugins/mamebridge/plugin.json'\\)\\); print\\('plugin.json: valid JSON'\\)\")",
"Bash(python3 -c \"import json,sys; json.load\\(open\\('.vscode/tasks.json'\\)\\); print\\('JSON OK'\\)\")",
"Bash(sysctl -n hw.ncpu)",
"Read(//Users/tolik/Documents/MAME/**)",
"Bash(claude mcp *)",
"Bash(python3 -c \"import mcp; print\\('mcp ok'\\)\")"
],
"additionalDirectories": [
"/Users/tolik/Documents/GitHub/mame/plugins/mamebridge"
]
}
}

205
src/CLAUDE.md Normal file
View File

@ -0,0 +1,205 @@
# 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.cpp` *does* support Z80:
shortnames `z80`, `z80n`, `z84c015` map 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/gdbstub` is 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 line `gdbstub: 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.lua` autoboot script polled via
`add_machine_frame_notifier`, which does not fire while the machine is
*hard-stopped* in the debugger. The `mamebridge` plugin instead pumps via
`emu.register_periodic()`, which is driven by `emulator_info::periodic_check()`
inside the debugger's `while (is_stopped())` loop (src/emu/debug/debugcpu.cpp).
So interactive stop→step→inspect loops now work.
- `mame_bridge.lua` is 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 via `register_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_DIR` IPC dir (default /tmp/mame_mcp)
- `MAME_MCP_CPU` CPU tag (default :maincpu)
- `MAME_MCP_SNAP_DIR` screenshot dir (default /tmp/mame_snap, cleared on reboot)
- `MAME_MCP_TIMEOUT` reply 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 via `screen: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 with
`cmd 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. Use `read_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+a*2+0x80*(m_rgmod&1))*1024+0x300+b*4 (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_watchpoint`
take 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 console `wpset 0x8000` quirkily 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 — `wpiset` for io space (the old
`wpset ADDR,1,w,1,1,i` form 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_key` maps 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_text` uses natkeyboard but targets only one keyboard and needs in_use; for
the firmware UI prefer `press_key`. Use `list_ports` to rediscover tags/masks.
## Environment gotchas (verified on the sprinter machine)
- `plugins/` here is symlinked from `~/Documents/MAME/plugins`, which is the
active `pluginspath`, so editing the repo files updates the running plugin.
- Run with the real config (see `~/Documents/MAME/Debug.sh`): `-bios dev` plus the
disk/floppy images. Without `-bios dev` the default `sp2k` BIOS ROM is missing
and the machine refuses to start.
- The `portname` plugin used the removed `emu.register_start`; it now uses
`emu.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 `mamed` at a time against a given `MAME_MCP_DIR`; a second instance
fails to start but a stale first instance keeps answering with old plugin code.
## Next actions
1. ~~Confirm banked vs flat memory.~~ DONE — banked.
2. ~~Turn `mame_bridge.lua` into a plugin (init.lua + plugin.json) for reliable
stop/step.~~ DONE — see `plugins/mamebridge/`.
3. Smoke test: load Z80 program, `set_breakpoint` at entry, `resume`, read regs.
Then verify the stop/step loop: at a breakpoint, `status` must reply
`state=stop` without timing out, and `step` + `read_registers` must advance PC
while staying stopped.

View File

@ -0,0 +1,504 @@
// license:BSD-3-Clause
// copyright-holders:Curt Coder
/**********************************************************************
Luxor 4105 SASI hard disk controller emulation
*********************************************************************/
/*
PCB Layout
----------
DATABOARD 4105-10
|-------------------------------------------|
|- LS367 LS74 LS32 LS00 LS38 LED|
|| -|
|| LS367 LS06 LS38 LS14 ||
|C C|
|N ALS08 LS08 81LS96 N|
|1 2|
|| 81LS95 74S373 LS273 74S240 ||
|| -|
|- SW1 DM8131 LS175 SW2 |
|-------------------------------------------|
Notes:
All IC's shown.
DM8131 - National Semiconductor DM8131N 6-Bit Unified Bus Comparator
LED - Power LED
SW1 - Drive settings
SW2 - Card address
CN1 - 2x32 PCB header, ABC 1600 bus
CN2 - 2x25 PCB header, Xebec S1410
*/
#include "emu.h"
#include "lux4105.h"
//**************************************************************************
// MACROS / CONSTANTS
//**************************************************************************
#define LOG 0
#define SASIBUS_TAG "sasi"
#define DMA_O1 BIT(m_dma, 0)
#define DMA_O2 BIT(m_dma, 1)
#define DMA_O3 BIT(m_dma, 2)
//**************************************************************************
// DEVICE DEFINITIONS
//**************************************************************************
DEFINE_DEVICE_TYPE(LUXOR_4105, luxor_4105_device, "lux4105", "Luxor 4105")
//-------------------------------------------------
// device_add_mconfig - add device configuration
//-------------------------------------------------
void luxor_4105_device::device_add_mconfig(machine_config &config)
{
auto &sasi(NSCSI_BUS(config, "sasi"));
NSCSI_CONNECTOR(config, "sasi:0", default_scsi_devices, "s1410");
sasi.set_external_device(7, *this);
}
//-------------------------------------------------
// INPUT_PORTS( luxor_4105 )
//-------------------------------------------------
INPUT_PORTS_START( luxor_4105 )
PORT_START("1E")
PORT_DIPNAME( 0x03, 0x03, "Stepping" ) PORT_DIPLOCATION("1E:1,2")
PORT_DIPSETTING( 0x00, DEF_STR( Normal ) )
PORT_DIPSETTING( 0x01, "Half (Seagate/Texas)" )
PORT_DIPSETTING( 0x02, "Half (Tandon)" )
PORT_DIPSETTING( 0x03, "Buffered" )
PORT_DIPNAME( 0x0c, 0x04, "Heads" ) PORT_DIPLOCATION("1E:3,4")
PORT_DIPSETTING( 0x00, "2" )
PORT_DIPSETTING( 0x04, "4" )
PORT_DIPSETTING( 0x08, "6" )
PORT_DIPSETTING( 0x0c, "8" )
PORT_DIPNAME( 0xf0, 0x30, "Drive Type" ) PORT_DIPLOCATION("1E:5,6,7,8")
PORT_DIPSETTING( 0x00, "Seagate ST506" )
PORT_DIPSETTING( 0x10, "Rodime RO100" )
PORT_DIPSETTING( 0x20, "Shugart SA600" )
PORT_DIPSETTING( 0x30, "Seagate ST412" )
PORT_START("5E")
PORT_DIPNAME( 0x3f, 0x25, "Card Address" ) PORT_DIPLOCATION("5E:1,2,3,4,5,6")
PORT_DIPSETTING( 0x25, "37" )
PORT_DIPSETTING( 0x2d, "45" )
PORT_START("S1")
PORT_CONFNAME( 0x01, 0x01, "DMA Timing" )
PORT_CONFSETTING( 0x00, "1043/1044" ) // a. TREN connected via delay circuit to OUT latch enable
PORT_CONFSETTING( 0x01, "1045/1046" ) // b. TREN connected directly to OUT latch enable
INPUT_PORTS_END
//-------------------------------------------------
// input_ports - device-specific input ports
//-------------------------------------------------
ioport_constructor luxor_4105_device::device_input_ports() const
{
return INPUT_PORTS_NAME( luxor_4105 );
}
//**************************************************************************
// LIVE DEVICE
//**************************************************************************
//-------------------------------------------------
// luxor_4105_device - constructor
//-------------------------------------------------
luxor_4105_device::luxor_4105_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
device_t(mconfig, LUXOR_4105, tag, owner, clock),
nscsi_device_interface(mconfig, *this),
device_abcbus_card_interface(mconfig, *this),
m_1e(*this, "1E"),
m_5e(*this, "5E"),
m_cs(false),
m_dma(0),
m_pren(1),
m_prac(1),
m_trrq(1)
{
}
//-------------------------------------------------
// device_start - device-specific startup
//-------------------------------------------------
void luxor_4105_device::device_start()
{
// state saving
save_item(NAME(m_cs));
save_item(NAME(m_data_out));
save_item(NAME(m_dma));
save_item(NAME(m_req));
save_item(NAME(m_drq));
save_item(NAME(m_pren));
save_item(NAME(m_trrq));
}
//-------------------------------------------------
// device_reset - device-specific reset
//-------------------------------------------------
void luxor_4105_device::device_reset()
{
m_cs = false;
internal_reset();
}
void luxor_4105_device::internal_reset()
{
write_dma_register(0);
m_data_out = 0;
m_scsi_bus->ctrl_w(m_scsi_refid, 0, S_SEL);
m_scsi_bus->ctrl_w(m_scsi_refid, S_RST, S_RST);
m_scsi_bus->ctrl_w(m_scsi_refid, 0, S_RST);
m_scsi_bus->ctrl_wait(m_scsi_refid, S_BSY|S_REQ|S_CTL|S_MSG|S_INP, S_ALL);
}
void luxor_4105_device::update_dma()
{
// TRRQ
bool req = (m_scsi_bus->ctrl_r() & S_REQ) && !m_req;
bool cd = m_scsi_bus->ctrl_r() & S_CTL;
m_trrq = !(req && !cd);
if (DMA_O2)
{
if (m_prac && !m_trrq)
{
// set REQ FF
m_req = 1;
update_ack();
req = (m_scsi_bus->ctrl_r() & S_REQ) && !m_req;
m_trrq = !(req && !cd);
}
}
m_slot->trrq_w(DMA_O2 ? m_trrq : 1);
// DRQ
bool io = m_scsi_bus->ctrl_r() & S_INP;
m_drq = (!((!cd || !io) && !(!cd && !DMA_O2))) && req;
// IRQ
bool irq = 1;
if (DMA_O2)
{
if (DMA_O1 && m_drq)
{
irq = 0;
}
}
else if (DMA_O3)
{
irq = 0;
}
m_slot->irq_w(irq);
}
void luxor_4105_device::update_ack()
{
m_scsi_bus->ctrl_w(m_scsi_refid, (m_scsi_bus->ctrl_r() & S_REQ && (m_scsi_bus->ctrl_r() & S_MSG || m_req)) ? S_ACK : 0, S_ACK);
}
void luxor_4105_device::write_dma_register(uint8_t data)
{
/*
bit description
0
1
2
3
4
5 byte interrupt enable?
6 DMA/CPU mode (1=DMA, 0=CPU)?
7 error interrupt enable?
*/
m_dma = BIT(data, 5) << 2 | BIT(data, 6) << 1 | BIT(data, 7);
if (LOG) logerror("%s DMA O1 %u O2 %u O3 %u\n", machine().describe_context(), DMA_O1, DMA_O2, DMA_O3);
// PREN
m_pren = !DMA_O2;
m_slot->pren_w(m_pren);
update_dma();
}
void luxor_4105_device::write_sasi_data(uint8_t data)
{
m_data_out = data;
if (!(m_scsi_bus->ctrl_r() & S_INP))
{
m_scsi_bus->data_w(m_scsi_refid, data);
}
// clock REQ FF
m_req = m_scsi_bus->ctrl_r() & S_REQ;
update_ack();
update_dma();
}
void luxor_4105_device::scsi_ctrl_changed()
{
if (m_scsi_bus->ctrl_r() & S_BSY)
m_scsi_bus->ctrl_w(m_scsi_refid, 0, S_SEL);
if (!(m_scsi_bus->ctrl_r() & S_REQ))
// reset REQ FF
m_req = 0;
if (m_scsi_bus->ctrl_r() & S_INP)
m_scsi_bus->data_w(m_scsi_refid, 0);
else
m_scsi_bus->data_w(m_scsi_refid, m_data_out);
update_ack();
update_dma();
}
//-------------------------------------------------
// abcbus_cs -
//-------------------------------------------------
void luxor_4105_device::abcbus_cs(uint8_t data)
{
m_cs = (data == m_5e->read());
}
//-------------------------------------------------
// abcbus_csb -
//-------------------------------------------------
int luxor_4105_device::abcbus_csb()
{
return !m_cs;
}
//-------------------------------------------------
// abcbus_stat -
//-------------------------------------------------
uint8_t luxor_4105_device::abcbus_stat()
{
uint8_t data = 0xff;
if (m_cs)
{
/*
bit description
0 REQ
1 C/D
2 BSY
3 I/O
4 0
5 DMA !O3
6 PREN
7 DMA request
*/
data = m_scsi_bus->ctrl_r() & S_REQ && !m_req;
data |= m_scsi_bus->ctrl_r() & S_CTL ? 0 : 2;
data |= m_scsi_bus->ctrl_r() & S_BSY ? 4 : 0;
data |= m_scsi_bus->ctrl_r() & S_INP ? 0 : 8;
data |= !DMA_O3 << 5;
data |= !m_pren << 6;
data |= !m_drq << 7;
}
return data;
}
//-------------------------------------------------
// abcbus_inp -
//-------------------------------------------------
uint8_t luxor_4105_device::abcbus_inp()
{
uint8_t data = 0xff;
if (m_cs)
{
if (m_scsi_bus->ctrl_r() & S_BSY)
{
data = m_scsi_bus->data_r();
}
else
{
data = m_1e->read();
}
// clock REQ FF
m_req = m_scsi_bus->ctrl_r() & S_REQ;
update_ack();
update_dma();
if (LOG) logerror("%s INP %02x\n", machine().describe_context(), data);
}
return data;
}
//-------------------------------------------------
// abcbus_utp -
//-------------------------------------------------
void luxor_4105_device::abcbus_out(uint8_t data)
{
if (m_cs)
{
if (LOG) logerror("%s OUT %02x\n", machine().describe_context(), data);
write_sasi_data(data);
}
}
//-------------------------------------------------
// abcbus_c1 -
//-------------------------------------------------
void luxor_4105_device::abcbus_c1(uint8_t data)
{
if (m_cs)
{
if (LOG) logerror("%s SELECT\n", machine().describe_context());
m_scsi_bus->ctrl_w(m_scsi_refid, S_SEL, S_SEL);
}
}
//-------------------------------------------------
// abcbus_c3 -
//-------------------------------------------------
void luxor_4105_device::abcbus_c3(uint8_t data)
{
if (m_cs)
{
if (LOG) logerror("%s RESET\n", machine().describe_context());
internal_reset();
}
}
//-------------------------------------------------
// abcbus_c4 -
//-------------------------------------------------
void luxor_4105_device::abcbus_c4(uint8_t data)
{
if (m_cs)
{
if (LOG) logerror("%s DMA %02x\n", machine().describe_context(), data);
write_dma_register(data);
}
}
//-------------------------------------------------
// abcbus_tren -
//-------------------------------------------------
uint8_t luxor_4105_device::abcbus_tren()
{
uint8_t data = 0xff;
if (DMA_O2)
{
if (m_scsi_bus->ctrl_r() & S_BSY)
{
data = m_scsi_bus->data_r();
}
// clock REQ FF
m_req = m_scsi_bus->ctrl_r() & S_REQ;
update_ack();
update_dma();
if (LOG) logerror("%s TREN R %02x\n", machine().describe_context(), data);
}
return data;
}
//-------------------------------------------------
// abcbus_tren -
//-------------------------------------------------
void luxor_4105_device::abcbus_tren(uint8_t data)
{
if (DMA_O2)
{
if (LOG) logerror("%s TREN W %02x\n", machine().describe_context(), data);
write_sasi_data(data);
}
}
//-------------------------------------------------
// abcbus_prac -
//-------------------------------------------------
void luxor_4105_device::abcbus_prac(int state)
{
if (LOG) logerror("%s PRAC %u\n", machine().describe_context(), state);
m_prac = state;
update_dma();
}

View File

@ -0,0 +1,85 @@
// license:BSD-3-Clause
// copyright-holders:Curt Coder
/**********************************************************************
Luxor 4105 SASI hard disk controller emulation
*********************************************************************/
#ifndef MAME_BUS_ABCBUS_LUX4105_H
#define MAME_BUS_ABCBUS_LUX4105_H
#pragma once
#include "abcbus.h"
#include "bus/nscsi/devices.h"
#include "machine/nscsi_bus.h"
//**************************************************************************
// TYPE DEFINITIONS
//**************************************************************************
// ======================> luxor_4105_device
class luxor_4105_device : public device_t,
public nscsi_device_interface,
public device_abcbus_card_interface
{
public:
// construction/destruction
luxor_4105_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock);
protected:
// device-level overrides
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
// optional information overrides
virtual void device_add_mconfig(machine_config &config) override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
// device_abcbus_interface overrides
virtual void abcbus_cs(uint8_t data) override;
virtual int abcbus_csb() override;
virtual uint8_t abcbus_inp() override;
virtual void abcbus_out(uint8_t data) override;
virtual uint8_t abcbus_stat() override;
virtual void abcbus_c1(uint8_t data) override;
virtual void abcbus_c3(uint8_t data) override;
virtual void abcbus_c4(uint8_t data) override;
virtual uint8_t abcbus_tren() override;
virtual void abcbus_tren(uint8_t data) override;
virtual void abcbus_prac(int state) override;
// nscsi_device_interface overrides
virtual void scsi_ctrl_changed() override;
private:
void internal_reset();
void update_ack();
void update_dma();
void write_dma_register(uint8_t data);
void write_sasi_data(uint8_t data);
required_ioport m_1e;
required_ioport m_5e;
bool m_cs;
uint8_t m_data_out;
uint8_t m_dma;
bool m_req;
bool m_drq;
bool m_pren;
bool m_prac;
bool m_trrq;
};
// device type definition
DECLARE_DEVICE_TYPE(LUXOR_4105, luxor_4105_device)
#endif // MAME_BUS_ABCBUS_LUX4105_H

View File

@ -1496,15 +1496,11 @@ void sprinter_state::init_taps()
{ {
// Internal z84 ports are not accessible through IO map, hence they need special case here // Internal z84 ports are not accessible through IO map, hence they need special case here
// Keep these in ascending order // Keep these in ascending order
constexpr u8 z84_int[] = { const u8 offset_8b = (offset & 0x00ff);
0x10, 0x11, 0x12, 0x13, // 0x10..0x13, 0x18..0x1F, 0xEE..0xF1, 0xF4
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, if ( (offset_8b == 0xf4) || ((offset_8b > 0x0f) && (offset_8b < 0x20) && ((offset_8b & 0x1c) != 0x14)) || ((offset_8b > 0xed) && (offset_8b < 0xf2)) ) {
0xee, 0xef,
0xf0, 0xf1, 0xf4
};
const auto found = std::lower_bound(std::begin(z84_int), std::end(z84_int), offset);
if ((found != std::end(z84_int)) && (*found == offset))
dcp_w(offset, data); dcp_w(offset, data);
}
}); });
} }

153
src/mame_bridge.lua Normal file
View File

@ -0,0 +1,153 @@
-- mame_bridge.lua
-- File-IPC bridge that exposes MAME's debugger to an external MCP server.
-- Launch MAME like:
-- mame <system> -debug -autoboot_script /path/to/mame_bridge.lua
--
-- Protocol: the MCP server drops a request file <DIR>/req_<id>.txt containing
-- one command line; this script processes it on each emulated frame and writes
-- the reply to <DIR>/resp_<id>.txt .
--
-- Env vars:
-- MAME_MCP_DIR IPC directory (default /tmp/mame_mcp)
-- MAME_MCP_CPU CPU device tag (default :maincpu)
local DIR = os.getenv("MAME_MCP_DIR") or "/tmp/mame_mcp"
local CPUTAG = os.getenv("MAME_MCP_CPU") or ":maincpu"
local SPACE = "program"
-- Z80 (and 8080-ish) registers worth reporting. Missing ones are skipped.
local REGS = { "PC","SP","AF","BC","DE","HL","IX","IY",
"AF2","BC2","DE2","HL2","I","R","IM","IFF1","IFF2","WZ" }
os.execute('mkdir -p "' .. DIR .. '" 2>/dev/null')
local lfs_ok, lfs = pcall(require, "lfs")
----------------------------------------------------------------------- helpers
local function read_file(p)
local f = io.open(p, "rb"); if not f then return nil end
local d = f:read("*a"); f:close(); return d
end
local function write_atomic(path, data)
local tmp = path .. ".tmp"
local f = io.open(tmp, "wb"); if not f then return false end
f:write(data); f:close()
os.rename(tmp, path)
return true
end
local function cpu() return manager.machine.devices[CPUTAG] end
local function dbg() return manager.machine.debugger end
local function space() return cpu().spaces[SPACE] end
-- Run a debugger console command and return any new console output it produced.
local function run_cmd(cmdstr)
local d = dbg()
local before = #d.consolelog
d:command(cmdstr)
local lines = {}
for i = before + 1, #d.consolelog do lines[#lines + 1] = d.consolelog[i] end
return table.concat(lines, "\n")
end
----------------------------------------------------------------------- actions
local function do_regs()
local c, out = cpu(), {}
for _, r in ipairs(REGS) do
local ok, ent = pcall(function() return c.state[r] end)
if ok and ent then
local ok2, v = pcall(function() return ent.value end)
if ok2 and v then out[#out + 1] = string.format("%s=0x%X", r, v) end
end
end
return table.concat(out, " ")
end
local function do_mem(addr, len)
local s, t = space(), {}
if len > 4096 then len = 4096 end
for i = 0, len - 1 do
local ok, b = pcall(function() return s:read_u8(addr + i) end)
t[#t + 1] = string.format("%02X", ok and b or 0)
end
return table.concat(t)
end
local function do_setmem(addr, hex)
local s, i = space(), 0
for byte in hex:gmatch("%x%x") do
s:write_u8(addr + i, tonumber(byte, 16)); i = i + 1
end
return "ok wrote " .. i .. " byte(s)"
end
local function do_status()
local st = dbg().execution_state
local pc = "?"
local ok, v = pcall(function() return cpu().state["PC"].value end)
if ok then pc = string.format("0x%X", v) end
return "state=" .. st .. " PC=" .. pc
end
local function do_dasm(addr, nbytes)
local tmp = DIR .. "/_dasm.tmp"
run_cmd(string.format("dasm %s,0x%X,%d", tmp, addr, nbytes))
local d = read_file(tmp) or "(no output)"
os.remove(tmp)
return d
end
--------------------------------------------------------------------- dispatch
local function dispatch(line)
if not manager.machine.debugger then
return "ERROR: debugger not enabled (start MAME with -debug)"
end
local words = {}
for w in line:gmatch("%S+") do words[#words + 1] = w end
local verb = words[1]
local ok, res = pcall(function()
if verb == "regs" then return do_regs()
elseif verb == "mem" then return do_mem(tonumber(words[2]), tonumber(words[3]))
elseif verb == "setmem" then return do_setmem(tonumber(words[2]), words[3])
elseif verb == "bp" then
local cond = line:match("^%S+%s+%S+%s+(.+)$")
return run_cmd("bpset " .. words[2] .. (cond and ("," .. cond) or ""))
elseif verb == "bpclr" then return run_cmd("bpclear " .. words[2])
elseif verb == "bplist" then return run_cmd("bplist")
elseif verb == "wp" then return run_cmd("wpset " .. words[2] .. "," .. words[3] .. "," .. words[4])
elseif verb == "wpclr" then return run_cmd("wpclear " .. words[2])
elseif verb == "step" then return run_cmd("step " .. (words[2] or "1"))
elseif verb == "over" then return run_cmd("over " .. (words[2] or "1"))
elseif verb == "out" then return run_cmd("out")
elseif verb == "cont" then dbg().execution_state = "run"; return "running"
elseif verb == "pause" then dbg().execution_state = "stop"; return "stopped"
elseif verb == "status" then return do_status()
elseif verb == "dasm" then return do_dasm(tonumber(words[2]), tonumber(words[3] or "32"))
elseif verb == "cmd" then return run_cmd(line:match("^cmd%s+(.+)$") or "")
else return "ERROR: unknown command '" .. tostring(verb) .. "'" end
end)
return ok and tostring(res) or ("ERROR: " .. tostring(res))
end
------------------------------------------------------------------------- pump
local function poll()
if not lfs_ok then return end
for entry in lfs.dir(DIR) do
local id = entry:match("^req_(%d+)%.txt$")
if id then
local reqp = DIR .. "/" .. entry
local data = read_file(reqp)
if data then
local reply = dispatch(data)
write_atomic(DIR .. "/resp_" .. id .. ".txt", reply)
os.remove(reqp)
end
end
end
end
-- Keep the subscription alive for the lifetime of the script.
local _sub = emu.add_machine_frame_notifier(function() poll() end)
emu.print_info("mame_bridge.lua active. IPC dir = " .. DIR)

270
src/mame_mcp.py Normal file
View File

@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
mame_mcp.py - MCP server that lets Claude Code drive the MAME debugger
(via mame_bridge.lua) for Z80/8080 retro-computer code.
Run standalone for a smoke test: python mame_mcp.py
Claude Code launches it over stdio automatically (see README).
Requires: pip install "mcp[cli]"
Optional env:
MAME_MCP_DIR IPC directory shared with the Lua script (default /tmp/mame_mcp)
MAME_MCP_TIMEOUT seconds to wait for a reply (default 10)
"""
import itertools
import os
import threading
import time
from mcp.server.fastmcp import FastMCP
DIR = os.environ.get("MAME_MCP_DIR", "/tmp/mame_mcp")
TIMEOUT = float(os.environ.get("MAME_MCP_TIMEOUT", "10"))
os.makedirs(DIR, exist_ok=True)
_ids = itertools.count(1)
_lock = threading.Lock()
mcp = FastMCP("mame-z80")
def _rpc(line: str) -> str:
"""Send one command line to the running MAME bridge and return its reply."""
with _lock:
rid = next(_ids)
req = os.path.join(DIR, f"req_{rid}.txt")
resp = os.path.join(DIR, f"resp_{rid}.txt")
tmp = req + ".tmp"
with open(tmp, "w") as f:
f.write(line.strip())
os.replace(tmp, req)
deadline = time.time() + TIMEOUT
while time.time() < deadline:
if os.path.exists(resp):
with open(resp) as f:
out = f.read()
for p in (resp, req):
try:
os.remove(p)
except OSError:
pass
return out or "(empty reply)"
time.sleep(0.02)
try:
os.remove(req)
except OSError:
pass
return ("ERROR: timed out. Is MAME running with "
"`-debug -autoboot_script mame_bridge.lua`, and is the machine running "
"(not hard-stopped in the debugger)?")
# --------------------------------------------------------------------- tools
@mcp.tool()
def read_registers() -> str:
"""Return the Z80/8080 CPU registers (PC, SP, AF, BC, DE, HL, IX, IY, ...)."""
return _rpc("regs")
@mcp.tool()
def read_memory(address: str, length: int = 16) -> str:
"""Read `length` bytes from the CPU program space at `address` (raw, no bank
translation). On Sprinter the Z80 windows live at program 0x10000+; use
read_logical_memory to read the Z80's 64K view. Hex like '0xC000' or decimal.
Returns a contiguous hex string. Max 4096 bytes."""
return _rpc(f"mem {address} {length}")
@mcp.tool()
def read_logical_memory(address: str, length: int = 16) -> str:
"""Read `length` bytes as the Z80 currently sees them through its banks, i.e.
logical address 0x0000-0xFFFF (mapped to Sprinter program 0x10000|addr).
This is what you usually want for code/data the running program touches."""
return _rpc(f"lmem {address} {length}")
@mcp.tool()
def read_vram(address: str, length: int = 16) -> str:
"""Read `length` bytes directly from the 256K video RAM share, bypassing Z80
banking. Use for inspecting screen pixel data, tile/mode descriptors, and the
palette (RGB triples at offset 0x3E0 within each 1K row). Max 4096 bytes."""
return _rpc(f"vram {address} {length}")
@mcp.tool()
def read_share(name: str, address: str, length: int = 16) -> str:
"""Read raw bytes from a named memory share (e.g. 'vram', 'fastram',
'video_ram'). See list_shares for available tags. Max 4096 bytes."""
return _rpc(f"share {name} {address} {length}")
@mcp.tool()
def list_shares() -> str:
"""List memory shares and regions (tags + sizes) for discovery."""
return _rpc("shares")
@mcp.tool()
def write_memory(address: str, hex_bytes: str) -> str:
"""Write bytes to the CPU program space. `hex_bytes` is a hex string, e.g.
'3E01CD0500'. (Note: this is raw program space; Sprinter Z80 windows are at
0x10000+, so use 0x10000|addr to write the Z80's logical view.)"""
return _rpc(f"setmem {address} {hex_bytes.replace(' ', '')}")
@mcp.tool()
def set_breakpoint(address: str, condition: str = "") -> str:
"""Set a PC breakpoint at `address`. Optional `condition` is a MAME debugger
expression (e.g. 'A==0'); the break only fires when it is true.
Returns the breakpoint index."""
return _rpc(f"bp {address} {condition}".strip())
@mcp.tool()
def clear_breakpoint(index: int) -> str:
"""Remove the breakpoint with the given index."""
return _rpc(f"bpclr {index}")
@mcp.tool()
def list_breakpoints() -> str:
"""List all active breakpoints."""
return _rpc("bplist")
@mcp.tool()
def set_watchpoint(address: str, length: int, access: str = "w",
space: str = "program") -> str:
"""Break when memory at `address` (over `length` bytes) is accessed.
`access` is 'r', 'w', or 'rw'. `space` selects the address space:
'program' (default), 'data', 'io', or 'opcodes' e.g. space='io' to catch
Z80 IN/OUT on a port (maps to the debugger's wpiset). Returns the index."""
return _rpc(f"wp {address} {length} {access} {space}")
@mcp.tool()
def clear_watchpoint(index: int) -> str:
"""Remove the watchpoint with the given index."""
return _rpc(f"wpclr {index}")
@mcp.tool()
def step(count: int = 1) -> str:
"""Single-step `count` instructions (requires the machine to be stopped)."""
return _rpc(f"step {count}")
@mcp.tool()
def step_over(count: int = 1) -> str:
"""Step over `count` instructions (CALLs run to completion)."""
return _rpc(f"over {count}")
@mcp.tool()
def step_out() -> str:
"""Run until the current subroutine returns."""
return _rpc("out")
@mcp.tool()
def resume() -> str:
"""Resume free-running execution."""
return _rpc("cont")
@mcp.tool()
def pause() -> str:
"""Stop execution and break into the debugger."""
return _rpc("pause")
@mcp.tool()
def status() -> str:
"""Report whether the CPU is running or stopped, plus the current PC."""
return _rpc("status")
@mcp.tool()
def disassemble(address: str, num_bytes: int = 32) -> str:
"""Disassemble `num_bytes` bytes starting at `address`."""
return _rpc(f"dasm {address} {num_bytes}")
@mcp.tool()
def screenshot(name: str = "") -> str:
"""Save a PNG screenshot of the emulator's active screen and return its full
path (which the caller can then open/view). With no `name`, a timestamped file
is generated. `name` may be a bare filename (placed in the snapshot temp dir,
/tmp/mame_snap by default, cleared on reboot) or an absolute path.
Note: the snapshot is the last drawn frame; while the machine is hard-stopped
the image is frozen, so resume() briefly before snapping for a fresh frame."""
return _rpc(f"snap {name}".strip())
@mcp.tool()
def list_ports() -> str:
"""List input ports and their fields (tag, bit mask, name) for discovery —
e.g. ':kbd:ms_naturl:*' (PC keyboard), ':IO_LINE0..7' (ZX matrix), ':JOY1/2',
':mouse_input1/2/3', ':rs232:microsoft_mouse:*'."""
return _rpc("ports")
@mcp.tool()
def press_key(key: str, frames: int = 3) -> str:
"""Press a NAMED key for `frames` emulated frames, then auto-release. Drives
BOTH the PC (ms_naturl) and ZX-Spectrum keyboards where applicable, like real
host input. Names: 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,
and symbols ; ' / . , - = [ ] \\ `. The machine must be RUNNING (resume first)."""
return _rpc(f"key {key} {frames}")
@mcp.tool()
def type_text(text: str, coded: bool = False) -> str:
"""Type text via the natural keyboard. If `coded`, honours {KEY} codes like
'dir{ENTER}'. Note: works for whichever keyboard MAME's natkeyboard targets;
for the firmware UI prefer press_key. Machine must be RUNNING."""
return _rpc((f"typecode {text}" if coded else f"type {text}"))
@mcp.tool()
def move_mouse(dx: int, dy: int, frames: int = 3) -> str:
"""Move BOTH mice (Kempston + RS232 COM) by relative dx,dy held over `frames`
frames (relative axes accumulate). Machine must be RUNNING."""
return _rpc(f"mouse {dx} {dy} {frames}")
@mcp.tool()
def click_mouse(button: str = "left", frames: int = 3) -> str:
"""Click a mouse button (left|right|middle) on BOTH mice for `frames` frames.
Machine must be RUNNING."""
return _rpc(f"mclick {button} {frames}")
@mcp.tool()
def press_input(port: str, mask: str, frames: int = 2) -> str:
"""Low-level: press a digital input field by port tag + bit mask for `frames`
frames (e.g. press_input(':JOY1', '0x400') = P1 button A). See list_ports."""
return _rpc(f"press {port} {mask} {frames}")
@mcp.tool()
def set_input(port: str, mask: str, value: int, frames: int = 0) -> str:
"""Low-level: set an input field (analog or digital) to `value`, optionally
held for `frames` frames. Use hold/release semantics via frames=0 (set once)."""
return _rpc(f"analog {port} {mask} {value} {frames}")
@mcp.tool()
def debugger_command(raw: str) -> str:
"""Escape hatch: run any MAME debugger console command verbatim
(e.g. 'history', 'trace mytrace.log', 'print pc'). Returns console output."""
return _rpc(f"cmd {raw}")
if __name__ == "__main__":
mcp.run()

View File

@ -160,6 +160,10 @@ void mac_osd_interface::check_osd_inputs()
if (machine().ui_input().pressed(IPT_OSD_8)) if (machine().ui_input().pressed(IPT_OSD_8))
window->renderer().record(); window->renderer().record();
// toggle releasing the pointer back to the OS
if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER))
toggle_pointer_release();
} }
//============================================================ //============================================================

View File

@ -28,10 +28,126 @@
#include "modules/osdmodule.h" #include "modules/osdmodule.h"
#include "zippath.h" #include "zippath.h"
#include "debugkeyconfig.h"
#include "util/xmlfile.h"
namespace osd { namespace osd {
namespace { namespace {
//-------------------------------------------------
// key name <-> ImGuiKey mapping for shortcuts
//-------------------------------------------------
struct imgui_key_name
{
char const *name;
ImGuiKey key;
};
imgui_key_name const f_imgui_keys[] = {
{ "F1", ImGuiKey_F1 }, { "F2", ImGuiKey_F2 }, { "F3", ImGuiKey_F3 }, { "F4", ImGuiKey_F4 },
{ "F5", ImGuiKey_F5 }, { "F6", ImGuiKey_F6 }, { "F7", ImGuiKey_F7 }, { "F8", ImGuiKey_F8 },
{ "F9", ImGuiKey_F9 }, { "F10", ImGuiKey_F10 }, { "F11", ImGuiKey_F11 }, { "F12", ImGuiKey_F12 },
{ "A", ImGuiKey_A }, { "B", ImGuiKey_B }, { "C", ImGuiKey_C }, { "D", ImGuiKey_D },
{ "E", ImGuiKey_E }, { "F", ImGuiKey_F }, { "G", ImGuiKey_G }, { "H", ImGuiKey_H },
{ "I", ImGuiKey_I }, { "J", ImGuiKey_J }, { "K", ImGuiKey_K }, { "L", ImGuiKey_L },
{ "M", ImGuiKey_M }, { "N", ImGuiKey_N }, { "O", ImGuiKey_O }, { "P", ImGuiKey_P },
{ "Q", ImGuiKey_Q }, { "R", ImGuiKey_R }, { "S", ImGuiKey_S }, { "T", ImGuiKey_T },
{ "U", ImGuiKey_U }, { "V", ImGuiKey_V }, { "W", ImGuiKey_W }, { "X", ImGuiKey_X },
{ "Y", ImGuiKey_Y }, { "Z", ImGuiKey_Z },
{ "0", ImGuiKey_0 }, { "1", ImGuiKey_1 }, { "2", ImGuiKey_2 }, { "3", ImGuiKey_3 },
{ "4", ImGuiKey_4 }, { "5", ImGuiKey_5 }, { "6", ImGuiKey_6 }, { "7", ImGuiKey_7 },
{ "8", ImGuiKey_8 }, { "9", ImGuiKey_9 },
{ "Up", ImGuiKey_UpArrow }, { "Down", ImGuiKey_DownArrow },
{ "Left", ImGuiKey_LeftArrow }, { "Right", ImGuiKey_RightArrow },
{ "Home", ImGuiKey_Home }, { "End", ImGuiKey_End },
{ "PageUp", ImGuiKey_PageUp }, { "PageDown", ImGuiKey_PageDown },
{ "Insert", ImGuiKey_Insert }, { "Delete", ImGuiKey_Delete },
{ "Space", ImGuiKey_Space }, { "Enter", ImGuiKey_Enter }, { "Tab", ImGuiKey_Tab }
};
ImGuiKey imgui_key_for_name(std::string const &name)
{
for (imgui_key_name const &entry : f_imgui_keys)
if (name == entry.name)
return entry.key;
return ImGuiKey_None;
}
bool shortcut_pressed(osd::debugger::key_shortcut const &sc)
{
if (sc.empty())
return false;
ImGuiKey const key = imgui_key_for_name(sc.key);
if (key == ImGuiKey_None)
return false;
ImGuiIO const &io = ImGui::GetIO();
if ((sc.ctrl != io.KeyCtrl) || (sc.shift != io.KeyShift) || (sc.alt != io.KeyAlt))
return false;
return ImGui::IsKeyPressed(key, false);
}
// detect a freshly pressed shortcut for the bindings recorder (returns whether one was captured)
bool capture_shortcut(osd::debugger::key_shortcut &out)
{
ImGuiIO const &io = ImGui::GetIO();
for (imgui_key_name const &entry : f_imgui_keys)
{
if (ImGui::IsKeyPressed(entry.key, false))
{
out.key = entry.name;
out.ctrl = io.KeyCtrl;
out.shift = io.KeyShift;
out.alt = io.KeyAlt;
return true;
}
}
return false;
}
// action identifiers (also used as keys in the configuration file)
char const *const IMGUI_ACT_NEW_DISASM = "new_disasm";
char const *const IMGUI_ACT_NEW_MEMORY = "new_memory";
char const *const IMGUI_ACT_NEW_BPOINTS = "new_bpoints";
char const *const IMGUI_ACT_NEW_WPOINTS = "new_wpoints";
char const *const IMGUI_ACT_NEW_LOG = "new_log";
char const *const IMGUI_ACT_RUN = "run";
char const *const IMGUI_ACT_RUN_NEXT_CPU = "run_next_cpu";
char const *const IMGUI_ACT_RUN_NEXT_INT = "run_next_int";
char const *const IMGUI_ACT_RUN_VBLANK = "run_vblank";
char const *const IMGUI_ACT_RUN_AND_HIDE = "run_and_hide";
char const *const IMGUI_ACT_STEP_INTO = "step_into";
char const *const IMGUI_ACT_STEP_OVER = "step_over";
char const *const IMGUI_ACT_STEP_OUT = "step_out";
char const *const IMGUI_ACT_SOFT_RESET = "soft_reset";
char const *const IMGUI_ACT_HARD_RESET = "hard_reset";
std::vector<osd::debugger::key_action> imgui_default_actions()
{
using osd::debugger::key_shortcut;
auto const sc = [] (char const *key, bool ctrl, bool shift) -> key_shortcut
{ key_shortcut s; s.key = key; s.ctrl = ctrl; s.shift = shift; return s; };
return {
{ IMGUI_ACT_NEW_DISASM, "New Disassembly Window", "Windows", sc("D", true, false) },
{ IMGUI_ACT_NEW_MEMORY, "New Memory Window", "Windows", sc("M", true, false) },
{ IMGUI_ACT_NEW_BPOINTS, "New Breakpoints Window", "Windows", sc("B", true, false) },
{ IMGUI_ACT_NEW_WPOINTS, "New Watchpoints Window", "Windows", sc("W", true, false) },
{ IMGUI_ACT_NEW_LOG, "New Log Window", "Windows", sc("L", true, false) },
{ IMGUI_ACT_RUN, "Run", "Execution", sc("F5", false, false) },
{ IMGUI_ACT_RUN_NEXT_CPU, "Go to Next CPU", "Execution", sc("F6", false, false) },
{ IMGUI_ACT_RUN_NEXT_INT, "Run until Next Interrupt", "Execution", sc("F7", false, false) },
{ IMGUI_ACT_RUN_VBLANK, "Run until Next VBLANK", "Execution", sc("F8", false, false) },
{ IMGUI_ACT_RUN_AND_HIDE, "Run and Hide Debugger", "Execution", sc("F12", false, false) },
{ IMGUI_ACT_STEP_INTO, "Single Step", "Execution", sc("F11", false, false) },
{ IMGUI_ACT_STEP_OVER, "Step Over", "Execution", sc("F10", false, false) },
{ IMGUI_ACT_STEP_OUT, "Step Out", "Execution", sc("F9", false, false) },
{ IMGUI_ACT_SOFT_RESET, "Soft Reset", "Execution", sc("F3", false, false) },
{ IMGUI_ACT_HARD_RESET, "Hard Reset", "Execution", sc("F3", false, true) }
};
}
class debug_area class debug_area
{ {
DISABLE_COPYING(debug_area); DISABLE_COPYING(debug_area);
@ -123,7 +239,9 @@ public:
m_create_open(false), m_create_open(false),
m_create_confirm_wait(false), m_create_confirm_wait(false),
m_selected_file(nullptr), m_selected_file(nullptr),
m_format_sel(0) m_format_sel(0),
m_keymap(imgui_default_actions()),
m_keybind_open(false)
{ {
} }
@ -177,6 +295,9 @@ private:
void draw_view(debug_area* view_ptr, bool exp_change); void draw_view(debug_area* view_ptr, bool exp_change);
void draw_mount_dialog(const char* label); void draw_mount_dialog(const char* label);
void draw_create_dialog(const char* label); void draw_create_dialog(const char* label);
void draw_keybindings();
void config_load(config_type cfgtype, config_level cfglevel, util::xml::data_node const *parentnode);
void config_save(config_type cfgtype, util::xml::data_node *parentnode);
void mount_image(); void mount_image();
void create_image(); void create_image();
void refresh_filelist(); void refresh_filelist();
@ -212,6 +333,10 @@ private:
int m_format_sel; int m_format_sel;
char m_path[1024]; // path text field buffer char m_path[1024]; // path text field buffer
std::unordered_map<input_item_id,ImGuiKey> m_mapping; std::unordered_map<input_item_id,ImGuiKey> m_mapping;
osd::debugger::keymap_config m_keymap; // remappable keyboard shortcuts
bool m_keybind_open; // true while the key bindings window is open
std::string m_keybind_recording; // id of the action currently being recorded ("" == none)
std::string m_keybind_status; // status line for the key bindings window
}; };
// globals // globals
@ -361,56 +486,57 @@ void debug_imgui::handle_events()
} }
} }
// global keys // don't fire global shortcuts while recording a new key binding
if(ImGui::IsKeyPressed(ImGuiKey_F3,false)) if(!m_keybind_recording.empty())
{ return;
if(ImGui::IsKeyDown(ImGuiKey_LeftShift))
m_machine->schedule_hard_reset();
else
{
m_machine->schedule_soft_reset();
m_machine->debugger().console().get_visible_cpu()->debug()->go();
}
}
if(ImGui::IsKeyPressed(ImGuiKey_F5,false)) // global keys (remappable - see m_keymap)
if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_SOFT_RESET)))
{
m_machine->schedule_soft_reset();
m_machine->debugger().console().get_visible_cpu()->debug()->go();
}
if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_HARD_RESET)))
m_machine->schedule_hard_reset();
if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_RUN)))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go(); m_machine->debugger().console().get_visible_cpu()->debug()->go();
m_running = true; m_running = true;
} }
if(ImGui::IsKeyPressed(ImGuiKey_F6,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_RUN_NEXT_CPU)))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go_next_device(); m_machine->debugger().console().get_visible_cpu()->debug()->go_next_device();
m_running = true; m_running = true;
} }
if(ImGui::IsKeyPressed(ImGuiKey_F7,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_RUN_NEXT_INT)))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go_interrupt(); m_machine->debugger().console().get_visible_cpu()->debug()->go_interrupt();
m_running = true; m_running = true;
} }
if(ImGui::IsKeyPressed(ImGuiKey_F8,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_RUN_VBLANK)))
m_machine->debugger().console().get_visible_cpu()->debug()->go_vblank(); m_machine->debugger().console().get_visible_cpu()->debug()->go_vblank();
if(ImGui::IsKeyPressed(ImGuiKey_F9,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_STEP_OUT)))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step_out(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step_out();
if(ImGui::IsKeyPressed(ImGuiKey_F10,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_STEP_OVER)))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step_over(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step_over();
if(ImGui::IsKeyPressed(ImGuiKey_F11,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_STEP_INTO)))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step();
if(ImGui::IsKeyPressed(ImGuiKey_F12,false)) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_RUN_AND_HIDE)))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go(); m_machine->debugger().console().get_visible_cpu()->debug()->go();
m_hide = true; m_hide = true;
} }
if(ImGui::IsKeyPressed(ImGuiKey_D,false) && io.KeyCtrl) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_DISASM)))
add_disasm(++m_win_count); add_disasm(++m_win_count);
if(ImGui::IsKeyPressed(ImGuiKey_M,false) && io.KeyCtrl) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_MEMORY)))
add_memory(++m_win_count); add_memory(++m_win_count);
if(ImGui::IsKeyPressed(ImGuiKey_B,false) && io.KeyCtrl) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_BPOINTS)))
add_bpoints(++m_win_count); add_bpoints(++m_win_count);
if(ImGui::IsKeyPressed(ImGuiKey_W,false) && io.KeyCtrl) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_WPOINTS)))
add_wpoints(++m_win_count); add_wpoints(++m_win_count);
if(ImGui::IsKeyPressed(ImGuiKey_L,false) && io.KeyCtrl) if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_LOG)))
add_log(++m_win_count); add_log(++m_win_count);
} }
@ -1312,47 +1438,65 @@ void debug_imgui::draw_console()
if(ImGui::BeginMenu("Debug")) if(ImGui::BeginMenu("Debug"))
{ {
show_menu = true; show_menu = true;
if(ImGui::MenuItem("New disassembly window", "Ctrl+D")) // shortcut labels reflect the (remappable) key bindings in m_keymap
std::string const sc_disasm = m_keymap.shortcut(IMGUI_ACT_NEW_DISASM).to_string();
std::string const sc_memory = m_keymap.shortcut(IMGUI_ACT_NEW_MEMORY).to_string();
std::string const sc_bpoints = m_keymap.shortcut(IMGUI_ACT_NEW_BPOINTS).to_string();
std::string const sc_wpoints = m_keymap.shortcut(IMGUI_ACT_NEW_WPOINTS).to_string();
std::string const sc_log = m_keymap.shortcut(IMGUI_ACT_NEW_LOG).to_string();
std::string const sc_run = m_keymap.shortcut(IMGUI_ACT_RUN).to_string();
std::string const sc_nextcpu = m_keymap.shortcut(IMGUI_ACT_RUN_NEXT_CPU).to_string();
std::string const sc_nextint = m_keymap.shortcut(IMGUI_ACT_RUN_NEXT_INT).to_string();
std::string const sc_vblank = m_keymap.shortcut(IMGUI_ACT_RUN_VBLANK).to_string();
std::string const sc_hide = m_keymap.shortcut(IMGUI_ACT_RUN_AND_HIDE).to_string();
std::string const sc_into = m_keymap.shortcut(IMGUI_ACT_STEP_INTO).to_string();
std::string const sc_over = m_keymap.shortcut(IMGUI_ACT_STEP_OVER).to_string();
std::string const sc_out = m_keymap.shortcut(IMGUI_ACT_STEP_OUT).to_string();
if(ImGui::MenuItem("New disassembly window", sc_disasm.c_str()))
add_disasm(++m_win_count); add_disasm(++m_win_count);
if(ImGui::MenuItem("New memory window", "Ctrl+M")) if(ImGui::MenuItem("New memory window", sc_memory.c_str()))
add_memory(++m_win_count); add_memory(++m_win_count);
if(ImGui::MenuItem("New breakpoints window", "Ctrl+B")) if(ImGui::MenuItem("New breakpoints window", sc_bpoints.c_str()))
add_bpoints(++m_win_count); add_bpoints(++m_win_count);
if(ImGui::MenuItem("New watchpoints window", "Ctrl+W")) if(ImGui::MenuItem("New watchpoints window", sc_wpoints.c_str()))
add_wpoints(++m_win_count); add_wpoints(++m_win_count);
if(ImGui::MenuItem("New log window", "Ctrl+L")) if(ImGui::MenuItem("New log window", sc_log.c_str()))
add_log(++m_win_count); add_log(++m_win_count);
ImGui::Separator(); ImGui::Separator();
if(ImGui::MenuItem("Run", "F5")) if(ImGui::MenuItem("Run", sc_run.c_str()))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go(); m_machine->debugger().console().get_visible_cpu()->debug()->go();
m_running = true; m_running = true;
} }
if(ImGui::MenuItem("Go to next CPU", "F6")) if(ImGui::MenuItem("Go to next CPU", sc_nextcpu.c_str()))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go_next_device(); m_machine->debugger().console().get_visible_cpu()->debug()->go_next_device();
m_running = true; m_running = true;
} }
if(ImGui::MenuItem("Run until next interrupt", "F7")) if(ImGui::MenuItem("Run until next interrupt", sc_nextint.c_str()))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go_interrupt(); m_machine->debugger().console().get_visible_cpu()->debug()->go_interrupt();
m_running = true; m_running = true;
} }
if(ImGui::MenuItem("Run until VBLANK", "F8")) if(ImGui::MenuItem("Run until VBLANK", sc_vblank.c_str()))
m_machine->debugger().console().get_visible_cpu()->debug()->go_vblank(); m_machine->debugger().console().get_visible_cpu()->debug()->go_vblank();
if(ImGui::MenuItem("Run and hide debugger", "F12")) if(ImGui::MenuItem("Run and hide debugger", sc_hide.c_str()))
{ {
m_machine->debugger().console().get_visible_cpu()->debug()->go(); m_machine->debugger().console().get_visible_cpu()->debug()->go();
m_hide = true; m_hide = true;
} }
ImGui::Separator(); ImGui::Separator();
if(ImGui::MenuItem("Single step", "F11")) if(ImGui::MenuItem("Single step", sc_into.c_str()))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step();
if(ImGui::MenuItem("Step over", "F10")) if(ImGui::MenuItem("Step over", sc_over.c_str()))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step_over(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step_over();
if(ImGui::MenuItem("Step out", "F9")) if(ImGui::MenuItem("Step out", sc_out.c_str()))
m_machine->debugger().console().get_visible_cpu()->debug()->single_step_out(); m_machine->debugger().console().get_visible_cpu()->debug()->single_step_out();
ImGui::Separator();
if(ImGui::MenuItem("Customize keys..."))
m_keybind_open = true;
ImGui::EndMenu(); ImGui::EndMenu();
} }
if(ImGui::BeginMenu("Window")) if(ImGui::BeginMenu("Window"))
@ -1448,6 +1592,8 @@ void debug_imgui::update()
ImGui::PushStyleColor(ImGuiCol_Border,ImVec4(0.7f,0.7f,0.7f,0.8f)); ImGui::PushStyleColor(ImGuiCol_Border,ImVec4(0.7f,0.7f,0.7f,0.8f));
m_text_size = ImGui::CalcTextSize("A"); // hopefully you're using a monospaced font... m_text_size = ImGui::CalcTextSize("A"); // hopefully you're using a monospaced font...
draw_console(); // We'll always have a console window draw_console(); // We'll always have a console window
if(m_keybind_open)
draw_keybindings();
view_ptr = view_list.begin(); view_ptr = view_list.begin();
while(view_ptr != view_list.end()) while(view_ptr != view_list.end())
@ -1489,6 +1635,126 @@ void debug_imgui::update()
ImGui::PopStyleColor(12); ImGui::PopStyleColor(12);
} }
void debug_imgui::config_load(config_type cfgtype, config_level cfglevel, util::xml::data_node const *parentnode)
{
// keyboard shortcuts are global - they live in default.cfg
if((config_type::DEFAULT == cfgtype) && parentnode)
m_keymap.load(*parentnode);
}
void debug_imgui::config_save(config_type cfgtype, util::xml::data_node *parentnode)
{
if((config_type::DEFAULT == cfgtype) && parentnode)
m_keymap.save(*parentnode);
}
void debug_imgui::draw_keybindings()
{
ImGui::SetNextWindowSize(ImVec2(480, 430), ImGuiCond_Once);
bool open = true;
if(ImGui::Begin("Customize Keys", &open))
{
ImGui::TextWrapped("Click a shortcut to record a new key combination. "
"While recording: Esc cancels, Delete/Backspace clears the binding.");
ImGui::Separator();
// while recording, watch for a key combination (or cancel/clear keys)
if(!m_keybind_recording.empty())
{
if(ImGui::IsKeyPressed(ImGuiKey_Escape, false))
{
m_keybind_recording.clear();
m_keybind_status = "Recording cancelled.";
}
else if(ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_Delete, false))
{
m_keymap.clear(m_keybind_recording);
m_keybind_status = "Cleared shortcut.";
m_keybind_recording.clear();
}
else
{
osd::debugger::key_shortcut sc;
if(capture_shortcut(sc))
{
std::string const conflict = m_keymap.conflicting_action(sc, m_keybind_recording);
if(!conflict.empty())
{
m_keybind_status = "\"" + sc.to_string() + "\" is already used - try another.";
}
else
{
m_keymap.set_shortcut(m_keybind_recording, sc);
m_keybind_status = "Set " + sc.to_string() + ".";
m_keybind_recording.clear();
}
}
}
}
ImVec2 const tableSize(0, -ImGui::GetFrameHeightWithSpacing() - ImGui::GetTextLineHeightWithSpacing());
if(ImGui::BeginTable("##keybinds", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, tableSize))
{
ImGui::TableSetupColumn("Action");
ImGui::TableSetupColumn("Group");
ImGui::TableSetupColumn("Shortcut");
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
for(osd::debugger::key_action const &action : m_keymap.actions())
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(action.label.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(action.group.c_str());
ImGui::TableNextColumn();
bool const recording = (m_keybind_recording == action.id);
std::string label;
if(recording)
{
label = "[press keys]";
}
else
{
label = m_keymap.shortcut(action.id).to_string();
if(label.empty())
label = "(none)";
}
label += "###"; // unique button id per action
label += action.id;
if(ImGui::SmallButton(label.c_str()))
{
m_keybind_recording = action.id;
m_keybind_status = "Recording shortcut for " + action.label + "...";
}
}
ImGui::EndTable();
}
ImGui::Separator();
if(ImGui::Button("Reset All to Defaults"))
{
m_keymap.reset_all();
m_keybind_recording.clear();
m_keybind_status = "All shortcuts reset to defaults.";
}
ImGui::SameLine();
if(ImGui::Button("Close"))
open = false;
if(!m_keybind_status.empty())
ImGui::TextDisabled("%s", m_keybind_status.c_str());
}
ImGui::End();
if(!open)
{
m_keybind_open = false;
m_keybind_recording.clear();
m_keybind_status.clear();
}
}
void debug_imgui::init_debugger(running_machine &machine) void debug_imgui::init_debugger(running_machine &machine)
{ {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
@ -1502,6 +1768,12 @@ void debug_imgui::init_debugger(running_machine &machine)
if (iter.first() != nullptr) if (iter.first() != nullptr)
m_has_images = true; m_has_images = true;
// register for configuration load/save so key bindings persist
machine.configuration().config_register(
"debugger",
configuration_manager::load_delegate(&debug_imgui::config_load, this),
configuration_manager::save_delegate(&debug_imgui::config_save, this));
// map keys to ImGui inputs // map keys to ImGui inputs
m_mapping[ITEM_ID_A] = ImGuiKey_A; m_mapping[ITEM_ID_A] = ImGuiKey_A;
m_mapping[ITEM_ID_C] = ImGuiKey_C; m_mapping[ITEM_ID_C] = ImGuiKey_C;
@ -1536,6 +1808,37 @@ void debug_imgui::init_debugger(running_machine &machine)
m_mapping[ITEM_ID_F10] = ImGuiKey_F10; m_mapping[ITEM_ID_F10] = ImGuiKey_F10;
m_mapping[ITEM_ID_F11] = ImGuiKey_F11; m_mapping[ITEM_ID_F11] = ImGuiKey_F11;
m_mapping[ITEM_ID_F12] = ImGuiKey_F12; m_mapping[ITEM_ID_F12] = ImGuiKey_F12;
// remaining keys so any shortcut can be bound/detected (see f_imgui_keys)
m_mapping[ITEM_ID_F1] = ImGuiKey_F1;
m_mapping[ITEM_ID_F2] = ImGuiKey_F2;
m_mapping[ITEM_ID_F4] = ImGuiKey_F4;
m_mapping[ITEM_ID_E] = ImGuiKey_E;
m_mapping[ITEM_ID_F] = ImGuiKey_F;
m_mapping[ITEM_ID_G] = ImGuiKey_G;
m_mapping[ITEM_ID_H] = ImGuiKey_H;
m_mapping[ITEM_ID_I] = ImGuiKey_I;
m_mapping[ITEM_ID_J] = ImGuiKey_J;
m_mapping[ITEM_ID_K] = ImGuiKey_K;
m_mapping[ITEM_ID_N] = ImGuiKey_N;
m_mapping[ITEM_ID_O] = ImGuiKey_O;
m_mapping[ITEM_ID_P] = ImGuiKey_P;
m_mapping[ITEM_ID_Q] = ImGuiKey_Q;
m_mapping[ITEM_ID_R] = ImGuiKey_R;
m_mapping[ITEM_ID_S] = ImGuiKey_S;
m_mapping[ITEM_ID_T] = ImGuiKey_T;
m_mapping[ITEM_ID_U] = ImGuiKey_U;
m_mapping[ITEM_ID_0] = ImGuiKey_0;
m_mapping[ITEM_ID_1] = ImGuiKey_1;
m_mapping[ITEM_ID_2] = ImGuiKey_2;
m_mapping[ITEM_ID_3] = ImGuiKey_3;
m_mapping[ITEM_ID_4] = ImGuiKey_4;
m_mapping[ITEM_ID_5] = ImGuiKey_5;
m_mapping[ITEM_ID_6] = ImGuiKey_6;
m_mapping[ITEM_ID_7] = ImGuiKey_7;
m_mapping[ITEM_ID_8] = ImGuiKey_8;
m_mapping[ITEM_ID_9] = ImGuiKey_9;
m_mapping[ITEM_ID_SPACE] = ImGuiKey_Space;
m_mapping[ITEM_ID_INSERT] = ImGuiKey_Insert;
// set key delay and repeat rates // set key delay and repeat rates
io.KeyRepeatDelay = 0.400f; io.KeyRepeatDelay = 0.400f;

View File

@ -0,0 +1,218 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeyconfig.cpp - portable remappable debugger key bindings
//
//============================================================
#include "debugkeyconfig.h"
#include "xmlconfig.h"
#include "util/xmlfile.h"
#include <algorithm>
#include <cctype>
namespace osd::debugger {
namespace {
std::string trim(std::string_view s)
{
auto const notspace = [] (unsigned char c) { return !std::isspace(c); };
auto const begin = std::find_if(s.begin(), s.end(), notspace);
auto const end = std::find_if(s.rbegin(), std::string_view::const_reverse_iterator(begin), notspace).base();
return std::string(begin, end);
}
std::string to_lower(std::string_view s)
{
std::string result(s);
std::transform(result.begin(), result.end(), result.begin(), [] (unsigned char c) { return std::tolower(c); });
return result;
}
} // anonymous namespace
//**************************************************************************
// KEY SHORTCUT
//**************************************************************************
bool key_shortcut::operator==(key_shortcut const &that) const
{
return (key == that.key) && (ctrl == that.ctrl) && (alt == that.alt) && (shift == that.shift);
}
std::string key_shortcut::to_string() const
{
if (key.empty())
return std::string();
std::string result;
if (ctrl)
result += "Ctrl+";
if (alt)
result += "Alt+";
if (shift)
result += "Shift+";
result += key;
return result;
}
key_shortcut key_shortcut::from_string(std::string_view text)
{
key_shortcut result;
std::string::size_type start = 0;
std::string const s(text);
while (start <= s.length())
{
std::string::size_type const plus = s.find('+', start);
std::string const token = trim((plus == std::string::npos) ? s.substr(start) : s.substr(start, plus - start));
std::string const lower = to_lower(token);
bool const last = (plus == std::string::npos);
if (!last && ((lower == "ctrl") || (lower == "control") || (lower == "cmd") || (lower == "command")))
result.ctrl = true;
else if (!last && ((lower == "alt") || (lower == "option") || (lower == "opt")))
result.alt = true;
else if (!last && (lower == "shift"))
result.shift = true;
else if (!token.empty())
result.key = token; // the base key is whatever is not a known modifier (normally last)
if (last)
break;
start = plus + 1;
}
return result;
}
//**************************************************************************
// KEYMAP CONFIG
//**************************************************************************
keymap_config::keymap_config(std::vector<key_action> &&actions) :
m_actions(std::move(actions))
{
}
key_action const *keymap_config::find(std::string const &id) const
{
auto const it = std::find_if(
m_actions.begin(),
m_actions.end(),
[&id] (key_action const &a) { return a.id == id; });
return (it != m_actions.end()) ? &*it : nullptr;
}
key_shortcut const &keymap_config::shortcut(std::string const &id) const
{
auto const override = m_overrides.find(id);
if (override != m_overrides.end())
return override->second;
key_action const *const action = find(id);
return action ? action->default_shortcut : m_unbound;
}
bool keymap_config::is_default(std::string const &id) const
{
return m_overrides.find(id) == m_overrides.end();
}
std::string keymap_config::conflicting_action(key_shortcut const &sc, std::string const &exclude_id) const
{
if (sc.empty())
return std::string();
for (key_action const &action : m_actions)
{
if (action.id == exclude_id)
continue;
if (shortcut(action.id) == sc)
return action.id;
}
return std::string();
}
void keymap_config::set_shortcut(std::string const &id, key_shortcut const &sc)
{
key_action const *const action = find(id);
if (!action)
return;
if (sc == action->default_shortcut)
m_overrides.erase(id); // back to default - drop the override
else
m_overrides[id] = sc;
}
void keymap_config::clear(std::string const &id)
{
set_shortcut(id, key_shortcut());
}
void keymap_config::reset(std::string const &id)
{
m_overrides.erase(id);
}
void keymap_config::reset_all()
{
m_overrides.clear();
}
void keymap_config::save(util::xml::data_node &parentnode) const
{
if (m_overrides.empty())
return;
util::xml::data_node *const keymap = parentnode.add_child(NODE_KEYMAP, nullptr);
if (!keymap)
return;
// write in table order for a stable, readable config file
for (key_action const &action : m_actions)
{
auto const override = m_overrides.find(action.id);
if (override == m_overrides.end())
continue;
util::xml::data_node *const item = keymap->add_child(NODE_KEYMAP_ITEM, nullptr);
if (!item)
continue;
item->set_attribute(ATTR_KEYMAP_ACTION, action.id.c_str());
item->set_attribute(ATTR_KEYMAP_KEY, override->second.to_string().c_str());
}
}
void keymap_config::load(util::xml::data_node const &parentnode)
{
util::xml::data_node const *const keymap = parentnode.get_child(NODE_KEYMAP);
if (!keymap)
return;
for (util::xml::data_node const *item = keymap->get_child(NODE_KEYMAP_ITEM);
item;
item = item->get_next_sibling(NODE_KEYMAP_ITEM))
{
char const *const id = item->get_attribute_string(ATTR_KEYMAP_ACTION, nullptr);
if (!id || !find(id))
continue;
char const *const text = item->get_attribute_string(ATTR_KEYMAP_KEY, "");
set_shortcut(id, key_shortcut::from_string(text));
}
}
} // namespace osd::debugger

View File

@ -0,0 +1,104 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeyconfig.h - portable remappable debugger key bindings
//
// Framework-agnostic storage for the debugger's keyboard
// shortcuts. Each GUI debugger (Qt, ImGui, Windows) supplies
// its own table of actions with default shortcuts; this class
// holds the user's overrides and (de)serialises them through
// the debugger configuration XML so the bindings survive across
// sessions. Shortcuts use a portable textual form such as
// "Ctrl+Shift+F9" so the same config is understood everywhere.
//
//============================================================
#ifndef MAME_OSD_MODULES_DEBUGGER_DEBUGKEYCONFIG_H
#define MAME_OSD_MODULES_DEBUGGER_DEBUGKEYCONFIG_H
#pragma once
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace util::xml { class data_node; }
namespace osd::debugger {
//-------------------------------------------------
// key_shortcut - a key name plus modifier flags
//-------------------------------------------------
struct key_shortcut
{
std::string key; // canonical base key name, e.g. "F5", "D", "Up" (empty == unbound)
bool ctrl = false;
bool alt = false;
bool shift = false;
bool empty() const { return key.empty(); }
bool operator==(key_shortcut const &that) const;
bool operator!=(key_shortcut const &that) const { return !(*this == that); }
// portable textual form, e.g. "Ctrl+Shift+F9"
std::string to_string() const;
static key_shortcut from_string(std::string_view text);
};
//-------------------------------------------------
// key_action - one remappable action
//-------------------------------------------------
struct key_action
{
std::string id; // stable identifier stored in the config
std::string label; // human-readable name for the UI
std::string group; // grouping for the UI
key_shortcut default_shortcut; // built-in binding
};
//-------------------------------------------------
// keymap_config - the action table plus overrides
//-------------------------------------------------
class keymap_config
{
public:
explicit keymap_config(std::vector<key_action> &&actions);
std::vector<key_action> const &actions() const { return m_actions; }
// current binding (user override if present, otherwise the default)
key_shortcut const &shortcut(std::string const &id) const;
bool is_default(std::string const &id) const;
// returns the id of an action already bound to sc (skipping exclude_id), or "" if none
std::string conflicting_action(key_shortcut const &sc, std::string const &exclude_id) const;
// editing
void set_shortcut(std::string const &id, key_shortcut const &sc);
void clear(std::string const &id);
void reset(std::string const &id);
void reset_all();
bool has_overrides() const { return !m_overrides.empty(); }
// persistence - operate on the parent debugger configuration node
void save(util::xml::data_node &parentnode) const; // adds a NODE_KEYMAP child if there are overrides
void load(util::xml::data_node const &parentnode); // reads a NODE_KEYMAP child if present
private:
key_action const *find(std::string const &id) const;
std::vector<key_action> m_actions;
std::unordered_map<std::string, key_shortcut> m_overrides;
key_shortcut const m_unbound; // returned for unknown ids
};
} // namespace osd::debugger
#endif // MAME_OSD_MODULES_DEBUGGER_DEBUGKEYCONFIG_H

View File

@ -27,6 +27,7 @@
#include "debug_module.h" #include "debug_module.h"
#import "osx/debugconsole.h" #import "osx/debugconsole.h"
#import "osx/debugkeymap.h"
#import "osx/debugwindowhandler.h" #import "osx/debugwindowhandler.h"
#include "util/xmlfile.h" #include "util/xmlfile.h"
@ -215,37 +216,50 @@ void debugger_osx::build_menus()
[editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
[editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
NSMenu *const debugMenu = [[NSMenu alloc] initWithTitle:@"Debug"]; NSMenu *const debugMenu = [[NSMenu alloc] initWithTitle:@"Debug"];
item = [[NSApp mainMenu] insertItemWithTitle:@"Debug" action:NULL keyEquivalent:@"" atIndex:2]; item = [[NSApp mainMenu] insertItemWithTitle:@"Debug" action:NULL keyEquivalent:@"" atIndex:2];
[item setSubmenu:debugMenu]; [item setSubmenu:debugMenu];
[debugMenu release]; [debugMenu release];
[debugMenu addItemWithTitle:@"New Memory Window" [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Memory Window"
action:@selector(debugNewMemoryWindow:) action:@selector(debugNewMemoryWindow:)
keyEquivalent:@"d"]; keyEquivalent:@""]
[debugMenu addItemWithTitle:@"New Disassembly Window" forAction:MAMEDebugActionNewMemoryWindow];
action:@selector(debugNewDisassemblyWindow:) [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Disassembly Window"
keyEquivalent:@"a"]; action:@selector(debugNewDisassemblyWindow:)
[debugMenu addItemWithTitle:@"New Error Log Window" keyEquivalent:@""]
action:@selector(debugNewErrorLogWindow:) forAction:MAMEDebugActionNewDisassemblyWindow];
keyEquivalent:@"l"]; [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Error Log Window"
[debugMenu addItemWithTitle:@"New (Break|Watch)points Window" action:@selector(debugNewErrorLogWindow:)
action:@selector(debugNewPointsWindow:) keyEquivalent:@""]
keyEquivalent:@"b"]; forAction:MAMEDebugActionNewErrorLogWindow];
[debugMenu addItemWithTitle:@"New Devices Window" [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New (Break|Watch)points Window"
action:@selector(debugNewDevicesWindow:) action:@selector(debugNewPointsWindow:)
keyEquivalent:@"D"]; keyEquivalent:@""]
forAction:MAMEDebugActionNewPointsWindow];
[keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Devices Window"
action:@selector(debugNewDevicesWindow:)
keyEquivalent:@""]
forAction:MAMEDebugActionNewDevicesWindow];
[debugMenu addItem:[NSMenuItem separatorItem]]; [debugMenu addItem:[NSMenuItem separatorItem]];
[[debugMenu addItemWithTitle:@"Soft Reset" [keys applyToMenuItem:[debugMenu addItemWithTitle:@"Soft Reset"
action:@selector(debugSoftReset:) action:@selector(debugSoftReset:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionSoftReset];
[[debugMenu addItemWithTitle:@"Hard Reset" [keys applyToMenuItem:[debugMenu addItemWithTitle:@"Hard Reset"
action:@selector(debugHardReset:) action:@selector(debugHardReset:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:NSEventModifierFlagShift]; forAction:MAMEDebugActionHardReset];
[debugMenu addItem:[NSMenuItem separatorItem]];
[debugMenu addItemWithTitle:@"Customize Keys…"
action:@selector(showKeyBindings:)
keyEquivalent:@""];
NSMenu *const runMenu = [[NSMenu alloc] initWithTitle:@"Run"]; NSMenu *const runMenu = [[NSMenu alloc] initWithTitle:@"Run"];
item = [[NSApp mainMenu] insertItemWithTitle:@"Run" item = [[NSApp mainMenu] insertItemWithTitle:@"Run"
@ -255,51 +269,52 @@ void debugger_osx::build_menus()
[item setSubmenu:runMenu]; [item setSubmenu:runMenu];
[runMenu release]; [runMenu release];
[runMenu addItemWithTitle:@"Break" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Break"
action:@selector(debugBreak:) action:@selector(debugBreak:)
keyEquivalent:@""]; keyEquivalent:@""]
forAction:MAMEDebugActionBreak];
[runMenu addItem:[NSMenuItem separatorItem]]; [runMenu addItem:[NSMenuItem separatorItem]];
[[runMenu addItemWithTitle:@"Run" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run"
action:@selector(debugRun:) action:@selector(debugRun:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF5FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRun];
[[runMenu addItemWithTitle:@"Run and Hide Debugger" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run and Hide Debugger"
action:@selector(debugRunAndHide:) action:@selector(debugRunAndHide:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF12FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunAndHide];
[[runMenu addItemWithTitle:@"Run to Next CPU" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run to Next CPU"
action:@selector(debugRunToNextCPU:) action:@selector(debugRunToNextCPU:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF6FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextCPU];
[[runMenu addItemWithTitle:@"Run until Next Interrupt on Current CPU" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run until Next Interrupt on Current CPU"
action:@selector(debugRunToNextInterrupt:) action:@selector(debugRunToNextInterrupt:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF7FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextInterrupt];
[[runMenu addItemWithTitle:@"Run until Next VBLANK" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run until Next VBLANK"
action:@selector(debugRunToNextVBLANK:) action:@selector(debugRunToNextVBLANK:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF8FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextVBLANK];
[[runMenu addItemWithTitle:@"Run to Cursor" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run to Cursor"
action:@selector(debugRunToCursor:) action:@selector(debugRunToCursor:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToCursor];
[runMenu addItem:[NSMenuItem separatorItem]]; [runMenu addItem:[NSMenuItem separatorItem]];
[[runMenu addItemWithTitle:@"Step Into" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Into"
action:@selector(debugStepInto:) action:@selector(debugStepInto:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF11FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionStepInto];
[[runMenu addItemWithTitle:@"Step Over" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Over"
action:@selector(debugStepOver:) action:@selector(debugStepOver:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionStepOver];
[[runMenu addItemWithTitle:@"Step Out" [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Out"
action:@selector(debugStepOut:) action:@selector(debugStepOut:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:NSEventModifierFlagShift]; forAction:MAMEDebugActionStepOut];
} }
} }
@ -311,6 +326,19 @@ void debugger_osx::build_menus()
void debugger_osx::config_load(config_type cfgtype, config_level cfglevel, util::xml::data_node const *parentnode) void debugger_osx::config_load(config_type cfgtype, config_level cfglevel, util::xml::data_node const *parentnode)
{ {
// keyboard shortcuts are global - they live in default.cfg
if ((config_type::DEFAULT == cfgtype) && parentnode)
{
util::xml::data_node const *const keymap = parentnode->get_child(osd::debugger::NODE_KEYMAP);
if (keymap)
{
NSAutoreleasePool *const pool = [[NSAutoreleasePool alloc] init];
[[MAMEDebugKeyMap sharedKeyMap] restoreConfigurationFromNode:keymap];
[pool release];
}
return;
}
if ((config_type::SYSTEM == cfgtype) && parentnode) if ((config_type::SYSTEM == cfgtype) && parentnode)
{ {
if (m_console) if (m_console)
@ -335,6 +363,21 @@ void debugger_osx::config_load(config_type cfgtype, config_level cfglevel, util:
void debugger_osx::config_save(config_type cfgtype, util::xml::data_node *parentnode) void debugger_osx::config_save(config_type cfgtype, util::xml::data_node *parentnode)
{ {
// keyboard shortcuts are global - they live in default.cfg
if (config_type::DEFAULT == cfgtype)
{
NSAutoreleasePool *const pool = [[NSAutoreleasePool alloc] init];
util::xml::data_node *const keymap = parentnode->add_child(osd::debugger::NODE_KEYMAP, nullptr);
if (keymap)
{
[[MAMEDebugKeyMap sharedKeyMap] saveConfigurationToNode:keymap];
if (!keymap->get_first_child())
keymap->delete_node(); // no overrides - don't clutter default.cfg
}
[pool release];
return;
}
if ((config_type::SYSTEM == cfgtype) && m_console) if ((config_type::SYSTEM == cfgtype) && m_console)
{ {
NSAutoreleasePool *const pool = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *const pool = [[NSAutoreleasePool alloc] init];

View File

@ -61,7 +61,8 @@ public:
osd_module(OSD_DEBUG_PROVIDER, "qt"), osd_module(OSD_DEBUG_PROVIDER, "qt"),
debug_module(), debug_module(),
m_machine(nullptr), m_machine(nullptr),
m_mainwindow(nullptr) m_mainwindow(nullptr),
m_keymap(debugger::qt::qtDefaultKeyActions())
{ {
} }
@ -83,6 +84,8 @@ public:
virtual running_machine &machine() const override { return *m_machine; } virtual running_machine &machine() const override { return *m_machine; }
virtual osd::debugger::keymap_config &keymap() override { return m_keymap; }
private: private:
void configuration_load(config_type which_type, config_level level, util::xml::data_node const *parentnode); void configuration_load(config_type which_type, config_level level, util::xml::data_node const *parentnode);
void configuration_save(config_type which_type, util::xml::data_node *parentnode); void configuration_save(config_type which_type, util::xml::data_node *parentnode);
@ -91,6 +94,7 @@ private:
running_machine *m_machine; running_machine *m_machine;
debugger::qt::MainWindow *m_mainwindow; debugger::qt::MainWindow *m_mainwindow;
util::xml::file::ptr m_config; util::xml::file::ptr m_config;
osd::debugger::keymap_config m_keymap;
}; };
@ -191,6 +195,13 @@ void debug_qt::debugger_update()
void debug_qt::configuration_load(config_type which_type, config_level level, util::xml::data_node const *parentnode) void debug_qt::configuration_load(config_type which_type, config_level level, util::xml::data_node const *parentnode)
{ {
// keyboard shortcuts are global - they live in default.cfg
if ((config_type::DEFAULT == which_type) && parentnode)
{
m_keymap.load(*parentnode);
return;
}
// We only care about system configuration files for now // We only care about system configuration files for now
if ((config_type::SYSTEM == which_type) && parentnode) if ((config_type::SYSTEM == which_type) && parentnode)
{ {
@ -209,6 +220,13 @@ void debug_qt::configuration_load(config_type which_type, config_level level, ut
void debug_qt::configuration_save(config_type which_type, util::xml::data_node *parentnode) void debug_qt::configuration_save(config_type which_type, util::xml::data_node *parentnode)
{ {
// keyboard shortcuts are global - they live in default.cfg
if ((config_type::DEFAULT == which_type) && parentnode)
{
m_keymap.save(*parentnode);
return;
}
// We only save system configuration for now // We only save system configuration for now
if ((config_type::SYSTEM == which_type) && parentnode) if ((config_type::SYSTEM == which_type) && parentnode)
emit saveConfiguration(*parentnode); emit saveConfiguration(*parentnode);

View File

@ -0,0 +1,105 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeymap.h - MacOS X Cocoa debug keyboard shortcut map
//
// Central registry of remappable debugger hot keys. Menu
// builders consult it instead of hard-coding key equivalents,
// the preferences window edits it, and it (de)serialises the
// user's overrides into the debugger configuration XML.
//
//============================================================
#ifndef MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAP_H
#define MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAP_H
#import <Cocoa/Cocoa.h>
#include "../xmlconfig.h"
namespace util::xml { class data_node; }
//============================================================
// Action identifiers (stable keys used in the config XML)
//============================================================
extern NSString *const MAMEDebugActionBreak;
extern NSString *const MAMEDebugActionRun;
extern NSString *const MAMEDebugActionRunAndHide;
extern NSString *const MAMEDebugActionRunToNextCPU;
extern NSString *const MAMEDebugActionRunToNextInterrupt;
extern NSString *const MAMEDebugActionRunToNextVBLANK;
extern NSString *const MAMEDebugActionRunToCursor;
extern NSString *const MAMEDebugActionStepInto;
extern NSString *const MAMEDebugActionStepOver;
extern NSString *const MAMEDebugActionStepOut;
extern NSString *const MAMEDebugActionSoftReset;
extern NSString *const MAMEDebugActionHardReset;
extern NSString *const MAMEDebugActionNewMemoryWindow;
extern NSString *const MAMEDebugActionNewDisassemblyWindow;
extern NSString *const MAMEDebugActionNewErrorLogWindow;
extern NSString *const MAMEDebugActionNewPointsWindow;
extern NSString *const MAMEDebugActionNewDevicesWindow;
extern NSString *const MAMEDebugActionCloseWindow;
extern NSString *const MAMEDebugActionQuit;
extern NSString *const MAMEDebugActionToggleBreakpoint;
extern NSString *const MAMEDebugActionDisableBreakpoint;
extern NSString *const MAMEDebugActionShowRawOpcodes;
extern NSString *const MAMEDebugActionShowEncryptedOpcodes;
extern NSString *const MAMEDebugActionShowComments;
// posted (object == shared keymap) whenever a binding changes so open menus can refresh
extern NSString *const MAMEDebugKeyMapChangedNotification;
//============================================================
// MAMEDebugKeyMap
//============================================================
@interface MAMEDebugKeyMap : NSObject
{
NSArray *order; // action identifiers in display order
NSDictionary *defaults; // identifier -> default MAMEDebugKeyBinding
NSMutableDictionary *current; // identifier -> current MAMEDebugKeyBinding
}
+ (MAMEDebugKeyMap *)sharedKeyMap;
// metadata for the preferences window
- (NSArray *)actionIdentifiers; // ordered list
- (NSString *)labelForAction:(NSString *)identifier; // human readable name
- (NSString *)groupForAction:(NSString *)identifier; // grouping name
// current binding for an action
- (NSString *)keyEquivalentForAction:(NSString *)identifier;
- (NSUInteger)modifierMaskForAction:(NSString *)identifier;
- (BOOL)isDefaultForAction:(NSString *)identifier;
// a printable description of the current shortcut (e.g. "⇧F9", "⌘D", "F5")
- (NSString *)displayStringForAction:(NSString *)identifier;
- (NSString *)displayStringForKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask;
// editing - posts MAMEDebugKeyMapChangedNotification
- (void)setKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask forAction:(NSString *)identifier;
- (void)clearAction:(NSString *)identifier; // remove the shortcut
- (void)resetActionToDefault:(NSString *)identifier;
- (void)resetAllToDefaults;
// returns the identifier of an action that already uses this shortcut (or nil), ignoring excludeIdentifier
- (NSString *)actionUsingKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask excluding:(NSString *)excludeIdentifier;
// apply the current binding for identifier to a freshly created menu item (also tags it for refreshMenu:)
- (void)applyToMenuItem:(NSMenuItem *)item forAction:(NSString *)identifier;
// re-apply current bindings to every tagged item in a menu tree (after a change)
- (void)refreshMenu:(NSMenu *)menu;
// persistence into the debugger configuration XML
- (void)saveConfigurationToNode:(util::xml::data_node *)node;
- (void)restoreConfigurationFromNode:(util::xml::data_node const *)node;
@end
#endif // MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAP_H

View File

@ -0,0 +1,391 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeymap.mm - MacOS X Cocoa debug keyboard shortcut map
//
//============================================================
#include "emu.h"
#import "debugkeymap.h"
#include "util/xmlfile.h"
//============================================================
// Action identifiers
//============================================================
NSString *const MAMEDebugActionBreak = @"break";
NSString *const MAMEDebugActionRun = @"run";
NSString *const MAMEDebugActionRunAndHide = @"runAndHide";
NSString *const MAMEDebugActionRunToNextCPU = @"runToNextCPU";
NSString *const MAMEDebugActionRunToNextInterrupt = @"runToNextInterrupt";
NSString *const MAMEDebugActionRunToNextVBLANK = @"runToNextVBLANK";
NSString *const MAMEDebugActionRunToCursor = @"runToCursor";
NSString *const MAMEDebugActionStepInto = @"stepInto";
NSString *const MAMEDebugActionStepOver = @"stepOver";
NSString *const MAMEDebugActionStepOut = @"stepOut";
NSString *const MAMEDebugActionSoftReset = @"softReset";
NSString *const MAMEDebugActionHardReset = @"hardReset";
NSString *const MAMEDebugActionNewMemoryWindow = @"newMemoryWindow";
NSString *const MAMEDebugActionNewDisassemblyWindow = @"newDisassemblyWindow";
NSString *const MAMEDebugActionNewErrorLogWindow = @"newErrorLogWindow";
NSString *const MAMEDebugActionNewPointsWindow = @"newPointsWindow";
NSString *const MAMEDebugActionNewDevicesWindow = @"newDevicesWindow";
NSString *const MAMEDebugActionCloseWindow = @"closeWindow";
NSString *const MAMEDebugActionQuit = @"quit";
NSString *const MAMEDebugActionToggleBreakpoint = @"toggleBreakpoint";
NSString *const MAMEDebugActionDisableBreakpoint = @"disableBreakpoint";
NSString *const MAMEDebugActionShowRawOpcodes = @"showRawOpcodes";
NSString *const MAMEDebugActionShowEncryptedOpcodes = @"showEncryptedOpcodes";
NSString *const MAMEDebugActionShowComments = @"showComments";
NSString *const MAMEDebugKeyMapChangedNotification = @"MAMEDebugKeyMapChangedNotification";
//============================================================
// MAMEDebugKeyBinding - a single editable shortcut
//============================================================
@interface MAMEDebugKeyBinding : NSObject
{
@public
NSString *key; // key equivalent (may be empty)
NSUInteger mask; // modifier flags
}
- (id)initWithKey:(NSString *)k mask:(NSUInteger)m;
@end
@implementation MAMEDebugKeyBinding
- (id)initWithKey:(NSString *)k mask:(NSUInteger)m {
if (!(self = [super init]))
return nil;
key = [(k ? k : @"") copy];
mask = m;
return self;
}
- (void)dealloc {
[key release];
[super dealloc];
}
@end
//============================================================
// MAMEDebugActionInfo - immutable per-action metadata + default
//============================================================
@interface MAMEDebugActionInfo : NSObject
{
@public
NSString *label;
NSString *group;
NSString *defaultKey;
NSUInteger defaultMask;
}
@end
@implementation MAMEDebugActionInfo
- (void)dealloc {
[label release];
[group release];
[defaultKey release];
[super dealloc];
}
@end
//============================================================
// helpers
//============================================================
static NSString *KeyForFunction(unichar c)
{
return [NSString stringWithFormat:@"%C", c];
}
@implementation MAMEDebugKeyMap
+ (MAMEDebugKeyMap *)sharedKeyMap {
static MAMEDebugKeyMap *instance = nil;
if (instance == nil)
instance = [[MAMEDebugKeyMap alloc] init];
return instance;
}
- (MAMEDebugActionInfo *)infoWithLabel:(NSString *)label
group:(NSString *)group
key:(NSString *)key
mask:(NSUInteger)mask {
MAMEDebugActionInfo *info = [[[MAMEDebugActionInfo alloc] init] autorelease];
info->label = [label copy];
info->group = [group copy];
info->defaultKey = [(key ? key : @"") copy];
info->defaultMask = mask;
return info;
}
- (id)init {
if (!(self = [super init]))
return nil;
NSUInteger const cmd = NSEventModifierFlagCommand;
NSUInteger const shift = NSEventModifierFlagShift;
// ordered table of actions: identifier -> metadata + default shortcut
NSMutableArray *ord = [NSMutableArray array];
NSMutableDictionary *def = [NSMutableDictionary dictionary];
void (^add)(NSString *, NSString *, NSString *, NSString *, NSUInteger) =
^(NSString *ident, NSString *label, NSString *group, NSString *key, NSUInteger mask) {
[ord addObject:ident];
[def setObject:[self infoWithLabel:label group:group key:key mask:mask] forKey:ident];
};
add(MAMEDebugActionBreak, @"Break", @"Execution", @"", 0);
add(MAMEDebugActionRun, @"Run", @"Execution", KeyForFunction(NSF5FunctionKey), 0);
add(MAMEDebugActionRunAndHide, @"Run and Hide Debugger", @"Execution", KeyForFunction(NSF12FunctionKey), 0);
add(MAMEDebugActionRunToNextCPU, @"Run to Next CPU", @"Execution", KeyForFunction(NSF6FunctionKey), 0);
add(MAMEDebugActionRunToNextInterrupt, @"Run until Next Interrupt on Current CPU", @"Execution", KeyForFunction(NSF7FunctionKey), 0);
add(MAMEDebugActionRunToNextVBLANK, @"Run until Next VBLANK", @"Execution", KeyForFunction(NSF8FunctionKey), 0);
add(MAMEDebugActionRunToCursor, @"Run to Cursor", @"Execution", KeyForFunction(NSF4FunctionKey), 0);
add(MAMEDebugActionStepInto, @"Step Into", @"Execution", KeyForFunction(NSF11FunctionKey), 0);
add(MAMEDebugActionStepOver, @"Step Over", @"Execution", KeyForFunction(NSF10FunctionKey), 0);
add(MAMEDebugActionStepOut, @"Step Out", @"Execution", KeyForFunction(NSF10FunctionKey), shift);
add(MAMEDebugActionSoftReset, @"Soft Reset", @"Execution", KeyForFunction(NSF3FunctionKey), 0);
add(MAMEDebugActionHardReset, @"Hard Reset", @"Execution", KeyForFunction(NSF3FunctionKey), shift);
add(MAMEDebugActionToggleBreakpoint, @"Toggle Breakpoint at Cursor", @"Disassembly", KeyForFunction(NSF9FunctionKey), 0);
add(MAMEDebugActionDisableBreakpoint, @"Disable Breakpoint at Cursor", @"Disassembly", KeyForFunction(NSF9FunctionKey), shift);
add(MAMEDebugActionShowRawOpcodes, @"Show Raw Opcodes", @"Disassembly", @"r", cmd);
add(MAMEDebugActionShowEncryptedOpcodes, @"Show Encrypted Opcodes", @"Disassembly", @"e", cmd);
add(MAMEDebugActionShowComments, @"Show Comments", @"Disassembly", @"n", cmd);
add(MAMEDebugActionNewMemoryWindow, @"New Memory Window", @"Windows", @"d", cmd);
add(MAMEDebugActionNewDisassemblyWindow, @"New Disassembly Window", @"Windows", @"a", cmd);
add(MAMEDebugActionNewErrorLogWindow, @"New Error Log Window", @"Windows", @"l", cmd);
add(MAMEDebugActionNewPointsWindow, @"New (Break|Watch)points Window", @"Windows", @"b", cmd);
add(MAMEDebugActionNewDevicesWindow, @"New Devices Window", @"Windows", @"D", cmd);
add(MAMEDebugActionCloseWindow, @"Close Window", @"Windows", @"w", cmd);
add(MAMEDebugActionQuit, @"Quit", @"Windows", @"q", cmd);
order = [ord copy];
defaults = [def copy];
current = [[NSMutableDictionary alloc] init];
for (NSString *ident in order)
{
MAMEDebugActionInfo *info = [defaults objectForKey:ident];
[current setObject:[[[MAMEDebugKeyBinding alloc] initWithKey:info->defaultKey mask:info->defaultMask] autorelease]
forKey:ident];
}
return self;
}
- (void)dealloc {
[order release];
[defaults release];
[current release];
[super dealloc];
}
- (NSArray *)actionIdentifiers {
return order;
}
- (NSString *)labelForAction:(NSString *)identifier {
MAMEDebugActionInfo *info = [defaults objectForKey:identifier];
return info ? info->label : identifier;
}
- (NSString *)groupForAction:(NSString *)identifier {
MAMEDebugActionInfo *info = [defaults objectForKey:identifier];
return info ? info->group : @"";
}
- (NSString *)keyEquivalentForAction:(NSString *)identifier {
MAMEDebugKeyBinding *b = [current objectForKey:identifier];
return b ? b->key : @"";
}
- (NSUInteger)modifierMaskForAction:(NSString *)identifier {
MAMEDebugKeyBinding *b = [current objectForKey:identifier];
return b ? b->mask : 0;
}
- (BOOL)isDefaultForAction:(NSString *)identifier {
MAMEDebugActionInfo *info = [defaults objectForKey:identifier];
MAMEDebugKeyBinding *b = [current objectForKey:identifier];
if (!info || !b)
return YES;
return (b->mask == info->defaultMask) && [b->key isEqualToString:info->defaultKey];
}
- (NSString *)displayStringForKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask {
if ([keyEquivalent length] == 0)
return @"None";
NSMutableString *result = [NSMutableString string];
if (mask & NSEventModifierFlagControl) [result appendString:@"⌃"]; // ⌃
if (mask & NSEventModifierFlagOption) [result appendString:@"⌥"]; // ⌥
if (mask & NSEventModifierFlagShift) [result appendString:@"⇧"]; // ⇧
if (mask & NSEventModifierFlagCommand) [result appendString:@"⌘"]; // ⌘
unichar c = [keyEquivalent characterAtIndex:0];
if (c >= NSF1FunctionKey && c <= NSF35FunctionKey)
[result appendFormat:@"F%d", (int)(c - NSF1FunctionKey + 1)];
else switch (c)
{
case NSUpArrowFunctionKey: [result appendString:@"↑"]; break; // ↑
case NSDownArrowFunctionKey: [result appendString:@"↓"]; break; // ↓
case NSLeftArrowFunctionKey: [result appendString:@"←"]; break; // ←
case NSRightArrowFunctionKey: [result appendString:@"→"]; break; // →
case 0x7F:
case NSDeleteFunctionKey: [result appendString:@"⌦"]; break; // ⌦
case NSHomeFunctionKey: [result appendString:@"Home"]; break;
case NSEndFunctionKey: [result appendString:@"End"]; break;
case NSPageUpFunctionKey: [result appendString:@"PgUp"]; break;
case NSPageDownFunctionKey: [result appendString:@"PgDn"]; break;
case 0x1B: [result appendString:@"⎋"]; break; // ⎋ esc
case 0x0D: case 0x03: [result appendString:@"↩"]; break; // ↩ return
case 0x09: [result appendString:@"⇥"]; break; // ⇥ tab
case ' ': [result appendString:@"Space"]; break;
default: [result appendString:[[keyEquivalent uppercaseString] substringToIndex:1]]; break;
}
return result;
}
- (NSString *)displayStringForAction:(NSString *)identifier {
return [self displayStringForKeyEquivalent:[self keyEquivalentForAction:identifier]
modifierMask:[self modifierMaskForAction:identifier]];
}
- (void)setKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask forAction:(NSString *)identifier {
if (![defaults objectForKey:identifier])
return;
[current setObject:[[[MAMEDebugKeyBinding alloc] initWithKey:keyEquivalent mask:mask] autorelease]
forKey:identifier];
[[NSNotificationCenter defaultCenter] postNotificationName:MAMEDebugKeyMapChangedNotification object:self];
}
- (void)clearAction:(NSString *)identifier {
[self setKeyEquivalent:@"" modifierMask:0 forAction:identifier];
}
- (void)resetActionToDefault:(NSString *)identifier {
MAMEDebugActionInfo *info = [defaults objectForKey:identifier];
if (info)
[self setKeyEquivalent:info->defaultKey modifierMask:info->defaultMask forAction:identifier];
}
- (void)resetAllToDefaults {
for (NSString *ident in order)
{
MAMEDebugActionInfo *info = [defaults objectForKey:ident];
[current setObject:[[[MAMEDebugKeyBinding alloc] initWithKey:info->defaultKey mask:info->defaultMask] autorelease]
forKey:ident];
}
[[NSNotificationCenter defaultCenter] postNotificationName:MAMEDebugKeyMapChangedNotification object:self];
}
- (NSString *)actionUsingKeyEquivalent:(NSString *)keyEquivalent modifierMask:(NSUInteger)mask excluding:(NSString *)excludeIdentifier {
if ([keyEquivalent length] == 0)
return nil;
for (NSString *ident in order)
{
if (excludeIdentifier && [ident isEqualToString:excludeIdentifier])
continue;
MAMEDebugKeyBinding *b = [current objectForKey:ident];
if (b && (b->mask == mask) && [b->key isEqualToString:keyEquivalent])
return ident;
}
return nil;
}
- (void)applyToMenuItem:(NSMenuItem *)item forAction:(NSString *)identifier {
[item setKeyEquivalent:[self keyEquivalentForAction:identifier]];
[item setKeyEquivalentModifierMask:[self modifierMaskForAction:identifier]];
[item setRepresentedObject:identifier];
}
- (void)refreshMenu:(NSMenu *)menu {
for (NSMenuItem *item in [menu itemArray])
{
id rep = [item representedObject];
if ([rep isKindOfClass:[NSString class]] && [defaults objectForKey:rep])
{
[item setKeyEquivalent:[self keyEquivalentForAction:rep]];
[item setKeyEquivalentModifierMask:[self modifierMaskForAction:rep]];
}
if ([item submenu])
[self refreshMenu:[item submenu]];
}
}
//============================================================
// persistence
//============================================================
- (void)saveConfigurationToNode:(util::xml::data_node *)node {
// only write entries that differ from the built-in defaults
for (NSString *ident in order)
{
if ([self isDefaultForAction:ident])
continue;
MAMEDebugKeyBinding *b = [current objectForKey:ident];
util::xml::data_node *const item = node->add_child(osd::debugger::NODE_KEYMAP_ITEM, nullptr);
if (!item)
continue;
item->set_attribute(osd::debugger::ATTR_KEYMAP_ACTION, [ident UTF8String]);
item->set_attribute_int(osd::debugger::ATTR_KEYMAP_KEY,
([b->key length] > 0) ? (int)[b->key characterAtIndex:0] : 0);
item->set_attribute_int(osd::debugger::ATTR_KEYMAP_MODIFIERS, (int)b->mask);
}
}
- (void)restoreConfigurationFromNode:(util::xml::data_node const *)node {
for (util::xml::data_node const *item = node->get_child(osd::debugger::NODE_KEYMAP_ITEM);
item;
item = item->get_next_sibling(osd::debugger::NODE_KEYMAP_ITEM))
{
char const *const action = item->get_attribute_string(osd::debugger::ATTR_KEYMAP_ACTION, nullptr);
if (!action)
continue;
NSString *const ident = [NSString stringWithUTF8String:action];
if (![defaults objectForKey:ident])
continue;
int const code = item->get_attribute_int(osd::debugger::ATTR_KEYMAP_KEY, 0);
NSUInteger const mask = (NSUInteger)item->get_attribute_int(osd::debugger::ATTR_KEYMAP_MODIFIERS, 0);
NSString *const key = (code > 0) ? [NSString stringWithFormat:@"%C", (unichar)code] : @"";
[current setObject:[[[MAMEDebugKeyBinding alloc] initWithKey:key mask:mask] autorelease]
forKey:ident];
}
[[NSNotificationCenter defaultCenter] postNotificationName:MAMEDebugKeyMapChangedNotification object:self];
}
@end

View File

@ -0,0 +1,43 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeymapviewer.h - MacOS X Cocoa debug keyboard shortcut editor
//
// A simple preferences window that lists every remappable
// debugger action and lets the user record a new shortcut for
// the selected one. Edits go straight into the shared
// MAMEDebugKeyMap, which persists them in the debugger config.
//
//============================================================
#ifndef MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAPVIEWER_H
#define MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAPVIEWER_H
#import <Cocoa/Cocoa.h>
@interface MAMEKeyBindingsWindow : NSObject <NSWindowDelegate, NSTableViewDataSource, NSTableViewDelegate>
{
NSWindow *window;
NSTableView *table;
NSButton *recordButton;
NSButton *clearButton;
NSButton *resetButton;
NSTextField *statusField;
NSArray *identifiers; // ordered action identifiers
id eventMonitor; // local key-down monitor while recording
BOOL recording;
}
// shared single-instance editor window
+ (MAMEKeyBindingsWindow *)sharedInstance;
- (void)activate;
- (IBAction)beginRecording:(id)sender;
- (IBAction)clearSelected:(id)sender;
- (IBAction)resetAll:(id)sender;
@end
#endif // MAME_OSD_MODULES_DEBUGGER_OSX_DEBUGKEYMAPVIEWER_H

View File

@ -0,0 +1,363 @@
// license:BSD-3-Clause
// copyright-holders:Vas Crabb
//============================================================
//
// debugkeymapviewer.mm - MacOS X Cocoa debug keyboard shortcut editor
//
//============================================================
#include "emu.h"
#import "debugkeymapviewer.h"
#import "debugkeymap.h"
@interface MAMEKeyBindingsWindow ()
- (void)stopRecording;
- (void)handleRecordedEvent:(NSEvent *)event;
- (void)keyMapChanged:(NSNotification *)notification;
@end
@implementation MAMEKeyBindingsWindow
+ (MAMEKeyBindingsWindow *)sharedInstance {
static MAMEKeyBindingsWindow *instance = nil;
if (instance == nil)
instance = [[MAMEKeyBindingsWindow alloc] init];
return instance;
}
- (id)init {
if (!(self = [super init]))
return nil;
identifiers = [[[MAMEDebugKeyMap sharedKeyMap] actionIdentifiers] copy];
recording = NO;
eventMonitor = nil;
NSRect const contentRect = NSMakeRect(0, 0, 460, 420);
window = [[NSWindow alloc] initWithContentRect:contentRect
styleMask:(NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable)
backing:NSBackingStoreBuffered
defer:YES];
[window setReleasedWhenClosed:NO];
[window setTitle:@"Debugger Keyboard Shortcuts"];
[window setContentMinSize:NSMakeSize(400, 300)];
[window setDelegate:self];
NSView *const content = [window contentView];
CGFloat const margin = 16;
CGFloat const buttonHeight = 32;
CGFloat const bottom = margin + buttonHeight + 8;
// table inside a scroll view
NSScrollView *const scroll = [[NSScrollView alloc] initWithFrame:NSMakeRect(margin,
bottom,
contentRect.size.width - (2 * margin),
contentRect.size.height - bottom - margin)];
[scroll setHasVerticalScroller:YES];
[scroll setHasHorizontalScroller:NO];
[scroll setAutohidesScrollers:YES];
[scroll setBorderType:NSBezelBorder];
[scroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
table = [[NSTableView alloc] initWithFrame:[[scroll contentView] bounds]];
[table setAllowsMultipleSelection:NO];
[table setAllowsColumnReordering:NO];
[table setUsesAlternatingRowBackgroundColors:YES];
[table setColumnAutoresizingStyle:NSTableViewLastColumnOnlyAutoresizingStyle];
NSTableColumn *const actionCol = [[NSTableColumn alloc] initWithIdentifier:@"action"];
[[actionCol headerCell] setStringValue:@"Action"];
[actionCol setWidth:230];
[actionCol setEditable:NO];
[table addTableColumn:actionCol];
[actionCol release];
NSTableColumn *const groupCol = [[NSTableColumn alloc] initWithIdentifier:@"group"];
[[groupCol headerCell] setStringValue:@"Group"];
[groupCol setWidth:100];
[groupCol setEditable:NO];
[table addTableColumn:groupCol];
[groupCol release];
NSTableColumn *const keyCol = [[NSTableColumn alloc] initWithIdentifier:@"shortcut"];
[[keyCol headerCell] setStringValue:@"Shortcut"];
[keyCol setWidth:90];
[keyCol setEditable:NO];
[table addTableColumn:keyCol];
[keyCol release];
[table setDataSource:self];
[table setDelegate:self];
[table setDoubleAction:@selector(beginRecording:)];
[table setTarget:self];
[scroll setDocumentView:table];
[content addSubview:scroll];
[scroll release];
// buttons along the bottom
recordButton = [[NSButton alloc] initWithFrame:NSMakeRect(margin, margin, 130, buttonHeight)];
[recordButton setButtonType:NSButtonTypeMomentaryPushIn];
[recordButton setBezelStyle:NSBezelStyleRounded];
[recordButton setTitle:@"Record Shortcut"];
[recordButton setTarget:self];
[recordButton setAction:@selector(beginRecording:)];
[recordButton setAutoresizingMask:NSViewMaxXMargin];
[content addSubview:recordButton];
clearButton = [[NSButton alloc] initWithFrame:NSMakeRect(margin + 134, margin, 80, buttonHeight)];
[clearButton setButtonType:NSButtonTypeMomentaryPushIn];
[clearButton setBezelStyle:NSBezelStyleRounded];
[clearButton setTitle:@"Clear"];
[clearButton setTarget:self];
[clearButton setAction:@selector(clearSelected:)];
[clearButton setAutoresizingMask:NSViewMaxXMargin];
[content addSubview:clearButton];
resetButton = [[NSButton alloc] initWithFrame:NSMakeRect(contentRect.size.width - margin - 150, margin, 150, buttonHeight)];
[resetButton setButtonType:NSButtonTypeMomentaryPushIn];
[resetButton setBezelStyle:NSBezelStyleRounded];
[resetButton setTitle:@"Reset All to Defaults"];
[resetButton setTarget:self];
[resetButton setAction:@selector(resetAll:)];
[resetButton setAutoresizingMask:NSViewMinXMargin];
[content addSubview:resetButton];
statusField = [[NSTextField alloc] initWithFrame:NSMakeRect(margin, bottom - 4, contentRect.size.width - (2 * margin), 16)];
[statusField setEditable:NO];
[statusField setSelectable:NO];
[statusField setBordered:NO];
[statusField setDrawsBackground:NO];
[statusField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[statusField setTextColor:[NSColor secondaryLabelColor]];
[statusField setStringValue:@"Double-click an action or press Record Shortcut, then type the new key combination."];
[statusField setAutoresizingMask:(NSViewWidthSizable | NSViewMaxYMargin)];
[content addSubview:statusField];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyMapChanged:)
name:MAMEDebugKeyMapChangedNotification
object:nil];
return self;
}
- (void)dealloc {
[self stopRecording];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[identifiers release];
[recordButton release];
[clearButton release];
[resetButton release];
[statusField release];
[table release];
if (window != nil)
{
[window orderOut:self];
[window release];
}
[super dealloc];
}
- (void)activate {
[table reloadData];
[window center];
[window makeKeyAndOrderFront:self];
}
- (void)keyMapChanged:(NSNotification *)notification {
// reflect external changes (e.g. config reload) in the table
if (!recording)
[table reloadData];
}
//============================================================
// table data source / delegate
//============================================================
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return [identifiers count];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row {
if ((row < 0) || (row >= (NSInteger)[identifiers count]))
return @"";
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
NSString *const ident = [identifiers objectAtIndex:row];
NSString *const colID = [column identifier];
if ([colID isEqualToString:@"action"])
return [keys labelForAction:ident];
else if ([colID isEqualToString:@"group"])
return [keys groupForAction:ident];
else
return [keys displayStringForAction:ident];
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification {
if (recording)
[self stopRecording];
}
//============================================================
// recording
//============================================================
- (void)stopRecording {
if (eventMonitor != nil)
{
[NSEvent removeMonitor:eventMonitor];
eventMonitor = nil;
}
recording = NO;
[recordButton setTitle:@"Record Shortcut"];
}
- (IBAction)beginRecording:(id)sender {
NSInteger const row = [table selectedRow];
if (row < 0)
{
[statusField setStringValue:@"Select an action first."];
NSBeep();
return;
}
if (recording)
{
[self stopRecording];
return;
}
recording = YES;
[recordButton setTitle:@"Press keys… (Esc to cancel)"];
NSString *const ident = [identifiers objectAtIndex:row];
[statusField setStringValue:[NSString stringWithFormat:@"Recording shortcut for \"%@\" — press a key combination (Delete clears, Esc cancels).",
[[MAMEDebugKeyMap sharedKeyMap] labelForAction:ident]]];
// the editor is a long-lived singleton, so capturing self here is safe
eventMonitor = [[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
handler:^NSEvent *(NSEvent *event) {
[self handleRecordedEvent:event];
return nil; // swallow the key event
}] retain];
}
- (void)handleRecordedEvent:(NSEvent *)event {
NSInteger const row = [table selectedRow];
if (row < 0)
{
[self stopRecording];
return;
}
NSString *const ident = [identifiers objectAtIndex:row];
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
NSString *const chars = [event charactersIgnoringModifiers];
unichar const code = ([chars length] > 0) ? [chars characterAtIndex:0] : 0;
// Escape cancels without changing anything
if (code == 0x1B)
{
[self stopRecording];
[statusField setStringValue:@"Recording cancelled."];
return;
}
// Delete / Backspace clears the binding
if ((code == 0x7F) || (code == NSDeleteCharacter) || (code == NSDeleteFunctionKey))
{
[keys clearAction:ident];
[self stopRecording];
[statusField setStringValue:[NSString stringWithFormat:@"Cleared shortcut for \"%@\".", [keys labelForAction:ident]]];
return;
}
if (code == 0)
{
[self stopRecording];
return;
}
NSUInteger const mask = [event modifierFlags] &
(NSEventModifierFlagCommand | NSEventModifierFlagOption |
NSEventModifierFlagControl | NSEventModifierFlagShift);
// store letters in lower case and rely on the shift flag, matching menu conventions
NSString *key = chars;
if ((code >= 'A') && (code <= 'Z'))
key = [chars lowercaseString];
// check for a conflicting assignment
NSString *const conflict = [keys actionUsingKeyEquivalent:key modifierMask:mask excluding:ident];
if (conflict)
{
NSString *const shortcut = [keys displayStringForKeyEquivalent:key modifierMask:mask];
NSAlert *const alert = [[[NSAlert alloc] init] autorelease];
[alert setMessageText:[NSString stringWithFormat:@"\"%@\" is already used by \"%@\".",
shortcut, [keys labelForAction:conflict]]];
[alert setInformativeText:@"Do you want to reassign it to the selected action?"];
[alert addButtonWithTitle:@"Reassign"];
[alert addButtonWithTitle:@"Cancel"];
[self stopRecording];
if ([alert runModal] == NSAlertFirstButtonReturn)
{
[keys clearAction:conflict];
[keys setKeyEquivalent:key modifierMask:mask forAction:ident];
[statusField setStringValue:[NSString stringWithFormat:@"Reassigned %@ to \"%@\".", shortcut, [keys labelForAction:ident]]];
}
else
{
[statusField setStringValue:@"Recording cancelled."];
}
return;
}
[keys setKeyEquivalent:key modifierMask:mask forAction:ident];
[self stopRecording];
[statusField setStringValue:[NSString stringWithFormat:@"Set %@ for \"%@\".",
[keys displayStringForAction:ident], [keys labelForAction:ident]]];
}
- (IBAction)clearSelected:(id)sender {
NSInteger const row = [table selectedRow];
if (row < 0)
{
NSBeep();
return;
}
NSString *const ident = [identifiers objectAtIndex:row];
[[MAMEDebugKeyMap sharedKeyMap] clearAction:ident];
[statusField setStringValue:[NSString stringWithFormat:@"Cleared shortcut for \"%@\".",
[[MAMEDebugKeyMap sharedKeyMap] labelForAction:ident]]];
}
- (IBAction)resetAll:(id)sender {
[[MAMEDebugKeyMap sharedKeyMap] resetAllToDefaults];
[statusField setStringValue:@"All shortcuts reset to defaults."];
}
//============================================================
// window delegate
//============================================================
- (void)windowWillClose:(NSNotification *)notification {
[self stopRecording];
}
@end

View File

@ -28,6 +28,8 @@
NSTextStorage *text; NSTextStorage *text;
NSTextContainer *textContainer; NSTextContainer *textContainer;
NSLayoutManager *layoutManager; NSLayoutManager *layoutManager;
CGGlyph glyphCache[256]; // monospaced glyph for each byte value (Core Text fast path)
} }
+ (NSFont *)defaultFontForMachine:(running_machine &)m; + (NSFont *)defaultFontForMachine:(running_machine &)m;

View File

@ -8,6 +8,8 @@
#import "debugview.h" #import "debugview.h"
#import <CoreText/CoreText.h>
#include "emu.h" #include "emu.h"
#include "debugger.h" #include "debugger.h"
#include "debug/debugcon.h" #include "debug/debugcon.h"
@ -18,6 +20,7 @@
#include "util/xmlfile.h" #include "util/xmlfile.h"
#include <cstring> #include <cstring>
#include <vector>
static NSColor *DefaultForeground; static NSColor *DefaultForeground;
@ -353,6 +356,15 @@ static void debugwin_view_update(debug_view &view, void *osdprivate)
fontWidth = [font maximumAdvancement].width; fontWidth = [font maximumAdvancement].width;
fontHeight = ceil([font ascender] - [font descender]); fontHeight = ceil([font ascender] - [font descender]);
fontAscent = [font ascender]; fontAscent = [font ascender];
// precompute the glyph for every byte value once, for the Core Text fast path in drawRect
{
UniChar chars[256];
for (int i = 0; i < 256; i++)
chars[i] = (UniChar)i;
CTFontGetGlyphsForCharacters((__bridge CTFontRef)font, chars, glyphCache, 256);
}
[[self enclosingScrollView] setLineScroll:fontHeight]; [[self enclosingScrollView] setLineScroll:fontHeight];
totalWidth = totalHeight = 0; totalWidth = totalHeight = 0;
[self update]; [self update];
@ -687,87 +699,77 @@ static void debugwin_view_update(debug_view &view, void *osdprivate)
debug_view_char const *data = view->viewdata(); debug_view_char const *data = view->viewdata();
if (!data) if (!data)
return; return;
// clear any space above the available content
data += ((row - origin.y) * size.x); data += ((row - origin.y) * size.x);
if (dirtyRect.origin.y < (row * fontHeight))
{
[DefaultBackground set];
[NSBezierPath fillRect:NSMakeRect(0,
dirtyRect.origin.y,
[self bounds].size.width,
(row * fontHeight) - dirtyRect.origin.y)];
}
// render entire lines to get character alignment right // the view is opaque: clear the whole dirty area to the default background first
for ( ; row < clip; row++, data += size.x) [DefaultBackground set];
NSRectFill(dirtyRect);
int32_t const rowStart = row;
debug_view_char const *const dataStart = data;
CGFloat const pad = [textContainer lineFragmentPadding];
// pass 1: backgrounds, in the view's normal (flipped) coordinates
data = dataStart;
for (int32_t r = rowStart; r < clip; r++, data += size.x)
{ {
int attr = -1; CGFloat const ytop = r * fontHeight;
NSUInteger start = 0, length = 0; for (int32_t col = 0; col < size.x; )
for (uint32_t col = origin.x; col < origin.x + size.x; col++)
{ {
[[text mutableString] appendFormat:@"%C", unichar(data[col - origin.x].byte)]; uint8_t const attr = data[col].attrib;
if ((start < length) && (attr != data[col - origin.x].attrib)) int32_t const start = col;
while ((col < size.x) && (data[col].attrib == attr))
col++;
NSColor *const bg = [self backgroundForAttribute:attr];
if (bg != DefaultBackground) // default already cleared above
{ {
NSRange const run = NSMakeRange(start, length - start); [bg set];
[text addAttribute:NSFontAttributeName NSRectFill(NSMakeRect(pad + ((origin.x + start) * fontWidth),
value:font ytop,
range:NSMakeRange(0, length)]; (col - start) * fontWidth,
[text addAttribute:NSForegroundColorAttributeName fontHeight));
value:[self foregroundForAttribute:attr]
range:run];
NSRange const glyphs = [layoutManager glyphRangeForCharacterRange:run
actualCharacterRange:NULL];
NSRect box = [layoutManager boundingRectForGlyphRange:glyphs
inTextContainer:textContainer];
if (start == 0)
{
box.size.width += box.origin.x;
box.origin.x = 0;
}
[[self backgroundForAttribute:attr] set];
[NSBezierPath fillRect:NSMakeRect(box.origin.x,
row * fontHeight,
box.size.width,
fontHeight)];
start = length;
} }
attr = data[col - origin.x].attrib;
length = [text length];
} }
NSRange const run = NSMakeRange(start, length - start);
[text addAttribute:NSFontAttributeName
value:font
range:NSMakeRange(0, length)];
[text addAttribute:NSForegroundColorAttributeName
value:[self foregroundForAttribute:attr]
range:run];
NSRange const glyphs = [layoutManager glyphRangeForCharacterRange:run
actualCharacterRange:NULL];
NSRect box = [layoutManager boundingRectForGlyphRange:glyphs
inTextContainer:textContainer];
if (start == 0)
box.origin.x = 0;
box.size.width = std::max([self bounds].size.width - box.origin.x, CGFloat(0));
[[self backgroundForAttribute:attr] set];
[NSBezierPath fillRect:NSMakeRect(box.origin.x,
row * fontHeight,
box.size.width,
fontHeight)];
[layoutManager drawGlyphsForGlyphRange:[layoutManager glyphRangeForTextContainer:textContainer]
atPoint:NSMakePoint(0, row * fontHeight)];
[text deleteCharactersInRange:NSMakeRange(0, length)];
} }
// clear any space below the available content // pass 2: glyphs, drawn directly on the fixed monospaced grid with Core Text
if ((dirtyRect.origin.y + dirtyRect.size.height) > (row * fontHeight)) // (no NSLayoutManager — the old per-row layout pass was O(rows*cols) and took
// >1s on large memory views, freezing the emulation on the shared main thread).
// Convert the flipped view to a y-up coordinate space so glyphs render upright.
CGContextRef const ctx = [[NSGraphicsContext currentContext] CGContext];
CTFontRef const ctFont = (__bridge CTFontRef)font;
CGFloat const boundsH = [self bounds].size.height;
std::vector<CGGlyph> glyphs(std::max<int32_t>(size.x, 1));
std::vector<CGPoint> positions(std::max<int32_t>(size.x, 1));
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0.0, boundsH);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
data = dataStart;
for (int32_t r = rowStart; r < clip; r++, data += size.x)
{ {
[DefaultBackground set]; CGFloat const baseline = boundsH - ((r * fontHeight) + fontAscent);
[NSBezierPath fillRect:NSMakeRect(0, for (int32_t col = 0; col < size.x; )
row * fontHeight, {
[self bounds].size.width, uint8_t const attr = data[col].attrib;
(dirtyRect.origin.y + dirtyRect.size.height) - (row * fontHeight))]; int32_t const start = col;
while ((col < size.x) && (data[col].attrib == attr))
col++;
int32_t const len = col - start;
CGFloat const x0 = pad + ((origin.x + start) * fontWidth);
for (int32_t k = 0; k < len; k++)
{
glyphs[k] = glyphCache[data[start + k].byte];
positions[k] = CGPointMake(x0 + (k * fontWidth), baseline);
}
[[self foregroundForAttribute:attr] set];
CTFontDrawGlyphs(ctFont, &glyphs[0], &positions[0], len, ctx);
}
} }
CGContextRestoreGState(ctx);
} }

View File

@ -14,7 +14,7 @@
@protocol MAMEDebugViewExpressionSupport; @protocol MAMEDebugViewExpressionSupport;
@class MAMEDebugCommandHistory, MAMEDebugConsole; @class MAMEDebugCommandHistory, MAMEDebugConsole, MAMEDebugKeyMap;
extern NSString *const MAMEHideDebuggerNotification; extern NSString *const MAMEHideDebuggerNotification;
@ -36,6 +36,9 @@ extern NSString *const MAMESaveDebuggerConfigurationNotification;
- (void)activate; - (void)activate;
- (IBAction)showKeyBindings:(id)sender;
- (void)keyMapChanged:(NSNotification *)notification;
- (IBAction)debugBreak:(id)sender; - (IBAction)debugBreak:(id)sender;
- (IBAction)debugRun:(id)sender; - (IBAction)debugRun:(id)sender;
- (IBAction)debugRunAndHide:(id)sender; - (IBAction)debugRunAndHide:(id)sender;

View File

@ -11,6 +11,8 @@
#import "debugconsole.h" #import "debugconsole.h"
#import "debugcommandhistory.h" #import "debugcommandhistory.h"
#import "debugkeymap.h"
#import "debugkeymapviewer.h"
#import "debugview.h" #import "debugview.h"
#include "debugger.h" #include "debugger.h"
@ -36,63 +38,66 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo
@implementation MAMEDebugWindowHandler @implementation MAMEDebugWindowHandler
+ (void)addCommonActionItems:(NSMenu *)menu { + (void)addCommonActionItems:(NSMenu *)menu {
[menu addItemWithTitle:@"Break" MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
action:@selector(debugBreak:)
keyEquivalent:@""]; [keys applyToMenuItem:[menu addItemWithTitle:@"Break"
action:@selector(debugBreak:)
keyEquivalent:@""]
forAction:MAMEDebugActionBreak];
NSMenuItem *runParentItem = [menu addItemWithTitle:@"Run" NSMenuItem *runParentItem = [menu addItemWithTitle:@"Run"
action:@selector(debugRun:) action:@selector(debugRun:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF5FunctionKey]]; keyEquivalent:@""];
NSMenu *runMenu = [[NSMenu alloc] initWithTitle:@"Run"]; NSMenu *runMenu = [[NSMenu alloc] initWithTitle:@"Run"];
[runParentItem setSubmenu:runMenu]; [runParentItem setSubmenu:runMenu];
[runMenu release]; [runMenu release];
[runParentItem setKeyEquivalentModifierMask:0]; [keys applyToMenuItem:runParentItem forAction:MAMEDebugActionRun];
[[runMenu addItemWithTitle:@"and Hide Debugger" [keys applyToMenuItem:[runMenu addItemWithTitle:@"and Hide Debugger"
action:@selector(debugRunAndHide:) action:@selector(debugRunAndHide:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF12FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunAndHide];
[[runMenu addItemWithTitle:@"to Next CPU" [keys applyToMenuItem:[runMenu addItemWithTitle:@"to Next CPU"
action:@selector(debugRunToNextCPU:) action:@selector(debugRunToNextCPU:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF6FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextCPU];
[[runMenu addItemWithTitle:@"until Next Interrupt on Current CPU" [keys applyToMenuItem:[runMenu addItemWithTitle:@"until Next Interrupt on Current CPU"
action:@selector(debugRunToNextInterrupt:) action:@selector(debugRunToNextInterrupt:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF7FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextInterrupt];
[[runMenu addItemWithTitle:@"until Next VBLANK" [keys applyToMenuItem:[runMenu addItemWithTitle:@"until Next VBLANK"
action:@selector(debugRunToNextVBLANK:) action:@selector(debugRunToNextVBLANK:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF8FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionRunToNextVBLANK];
NSMenuItem *stepParentItem = [menu addItemWithTitle:@"Step" action:NULL keyEquivalent:@""]; NSMenuItem *stepParentItem = [menu addItemWithTitle:@"Step" action:NULL keyEquivalent:@""];
NSMenu *stepMenu = [[NSMenu alloc] initWithTitle:@"Step"]; NSMenu *stepMenu = [[NSMenu alloc] initWithTitle:@"Step"];
[stepParentItem setSubmenu:stepMenu]; [stepParentItem setSubmenu:stepMenu];
[stepMenu release]; [stepMenu release];
[[stepMenu addItemWithTitle:@"Into" [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Into"
action:@selector(debugStepInto:) action:@selector(debugStepInto:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF11FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionStepInto];
[[stepMenu addItemWithTitle:@"Over" [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Over"
action:@selector(debugStepOver:) action:@selector(debugStepOver:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionStepOver];
[[stepMenu addItemWithTitle:@"Out" [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Out"
action:@selector(debugStepOut:) action:@selector(debugStepOut:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:NSEventModifierFlagShift]; forAction:MAMEDebugActionStepOut];
NSMenuItem *resetParentItem = [menu addItemWithTitle:@"Reset" action:NULL keyEquivalent:@""]; NSMenuItem *resetParentItem = [menu addItemWithTitle:@"Reset" action:NULL keyEquivalent:@""];
NSMenu *resetMenu = [[NSMenu alloc] initWithTitle:@"Reset"]; NSMenu *resetMenu = [[NSMenu alloc] initWithTitle:@"Reset"];
[resetParentItem setSubmenu:resetMenu]; [resetParentItem setSubmenu:resetMenu];
[resetMenu release]; [resetMenu release];
[[resetMenu addItemWithTitle:@"Soft" [keys applyToMenuItem:[resetMenu addItemWithTitle:@"Soft"
action:@selector(debugSoftReset:) action:@selector(debugSoftReset:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:0]; forAction:MAMEDebugActionSoftReset];
[[resetMenu addItemWithTitle:@"Hard" [keys applyToMenuItem:[resetMenu addItemWithTitle:@"Hard"
action:@selector(debugHardReset:) action:@selector(debugHardReset:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] keyEquivalent:@""]
setKeyEquivalentModifierMask:NSEventModifierFlagShift]; forAction:MAMEDebugActionHardReset];
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
@ -100,26 +105,41 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo
NSMenu *newMenu = [[NSMenu alloc] initWithTitle:@"New"]; NSMenu *newMenu = [[NSMenu alloc] initWithTitle:@"New"];
[newParentItem setSubmenu:newMenu]; [newParentItem setSubmenu:newMenu];
[newMenu release]; [newMenu release];
[newMenu addItemWithTitle:@"Memory Window" [keys applyToMenuItem:[newMenu addItemWithTitle:@"Memory Window"
action:@selector(debugNewMemoryWindow:) action:@selector(debugNewMemoryWindow:)
keyEquivalent:@"d"]; keyEquivalent:@""]
[newMenu addItemWithTitle:@"Disassembly Window" forAction:MAMEDebugActionNewMemoryWindow];
action:@selector(debugNewDisassemblyWindow:) [keys applyToMenuItem:[newMenu addItemWithTitle:@"Disassembly Window"
keyEquivalent:@"a"]; action:@selector(debugNewDisassemblyWindow:)
[newMenu addItemWithTitle:@"Error Log Window" keyEquivalent:@""]
action:@selector(debugNewErrorLogWindow:) forAction:MAMEDebugActionNewDisassemblyWindow];
keyEquivalent:@"l"]; [keys applyToMenuItem:[newMenu addItemWithTitle:@"Error Log Window"
[newMenu addItemWithTitle:@"(Break|Watch)points Window" action:@selector(debugNewErrorLogWindow:)
action:@selector(debugNewPointsWindow:) keyEquivalent:@""]
keyEquivalent:@"b"]; forAction:MAMEDebugActionNewErrorLogWindow];
[newMenu addItemWithTitle:@"Devices Window" [keys applyToMenuItem:[newMenu addItemWithTitle:@"(Break|Watch)points Window"
action:@selector(debugNewDevicesWindow:) action:@selector(debugNewPointsWindow:)
keyEquivalent:@""]; keyEquivalent:@""]
forAction:MAMEDebugActionNewPointsWindow];
[keys applyToMenuItem:[newMenu addItemWithTitle:@"Devices Window"
action:@selector(debugNewDevicesWindow:)
keyEquivalent:@""]
forAction:MAMEDebugActionNewDevicesWindow];
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:@"Close Window" action:@selector(performClose:) keyEquivalent:@"w"]; [keys applyToMenuItem:[menu addItemWithTitle:@"Close Window"
[menu addItemWithTitle:@"Quit" action:@selector(debugExit:) keyEquivalent:@"q"]; action:@selector(performClose:)
keyEquivalent:@""]
forAction:MAMEDebugActionCloseWindow];
[keys applyToMenuItem:[menu addItemWithTitle:@"Quit"
action:@selector(debugExit:)
keyEquivalent:@""]
forAction:MAMEDebugActionQuit];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:@"Customize Keys…" action:@selector(showKeyBindings:) keyEquivalent:@""];
} }
@ -163,6 +183,10 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo
selector:@selector(hideDebugger:) selector:@selector(hideDebugger:)
name:MAMEHideDebuggerNotification name:MAMEHideDebuggerNotification
object:nil]; object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyMapChanged:)
name:MAMEDebugKeyMapChangedNotification
object:nil];
machine = &m; machine = &m;
@ -188,6 +212,26 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo
} }
- (IBAction)showKeyBindings:(id)sender {
[[MAMEKeyBindingsWindow sharedInstance] activate];
}
- (void)refreshKeyEquivalentsInView:(NSView *)view withKeyMap:(MAMEDebugKeyMap *)keys {
if ([view isKindOfClass:[NSPopUpButton class]])
[keys refreshMenu:[(NSPopUpButton *)view menu]];
for (NSView *sub in [view subviews])
[self refreshKeyEquivalentsInView:sub withKeyMap:keys];
}
- (void)keyMapChanged:(NSNotification *)notification {
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
[keys refreshMenu:[NSApp mainMenu]];
[self refreshKeyEquivalentsInView:[window contentView] withKeyMap:keys];
}
- (IBAction)debugBreak:(id)sender { - (IBAction)debugBreak:(id)sender {
if (machine->debug_flags & DEBUG_FLAG_ENABLED) if (machine->debug_flags & DEBUG_FLAG_ENABLED)
machine->debugger().console().get_visible_cpu()->debug()->halt_on_next_instruction("User-initiated break\n"); machine->debugger().console().get_visible_cpu()->debug()->halt_on_next_instruction("User-initiated break\n");

View File

@ -9,6 +9,8 @@
#include "emu.h" #include "emu.h"
#import "disassemblyview.h" #import "disassemblyview.h"
#import "debugkeymap.h"
#include "debug/debugvw.h" #include "debug/debugvw.h"
#include "util/xmlfile.h" #include "util/xmlfile.h"
@ -60,6 +62,7 @@
- (void)addContextMenuItemsToMenu:(NSMenu *)menu { - (void)addContextMenuItemsToMenu:(NSMenu *)menu {
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
NSMenuItem *item; NSMenuItem *item;
[super addContextMenuItemsToMenu:menu]; [super addContextMenuItemsToMenu:menu];
@ -69,38 +72,41 @@
item = [menu addItemWithTitle:@"Toggle Breakpoint" item = [menu addItemWithTitle:@"Toggle Breakpoint"
action:@selector(debugToggleBreakpoint:) action:@selector(debugToggleBreakpoint:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey]]; keyEquivalent:@""];
[item setKeyEquivalentModifierMask:0]; [keys applyToMenuItem:item forAction:MAMEDebugActionToggleBreakpoint];
item = [menu addItemWithTitle:@"Disable Breakpoint" item = [menu addItemWithTitle:@"Disable Breakpoint"
action:@selector(debugToggleBreakpointEnable:) action:@selector(debugToggleBreakpointEnable:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey]]; keyEquivalent:@""];
[item setKeyEquivalentModifierMask:NSEventModifierFlagShift]; [keys applyToMenuItem:item forAction:MAMEDebugActionDisableBreakpoint];
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
item = [menu addItemWithTitle:@"Run to Cursor" item = [menu addItemWithTitle:@"Run to Cursor"
action:@selector(debugRunToCursor:) action:@selector(debugRunToCursor:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]]; keyEquivalent:@""];
[item setKeyEquivalentModifierMask:0]; [keys applyToMenuItem:item forAction:MAMEDebugActionRunToCursor];
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
item = [menu addItemWithTitle:@"Raw Opcodes" item = [menu addItemWithTitle:@"Raw Opcodes"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"r"]; keyEquivalent:@""];
[keys applyToMenuItem:item forAction:MAMEDebugActionShowRawOpcodes];
[item setTarget:self]; [item setTarget:self];
[item setTag:DASM_RIGHTCOL_RAW]; [item setTag:DASM_RIGHTCOL_RAW];
item = [menu addItemWithTitle:@"Encrypted Opcodes" item = [menu addItemWithTitle:@"Encrypted Opcodes"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"e"]; keyEquivalent:@""];
[keys applyToMenuItem:item forAction:MAMEDebugActionShowEncryptedOpcodes];
[item setTarget:self]; [item setTarget:self];
[item setTag:DASM_RIGHTCOL_ENCRYPTED]; [item setTag:DASM_RIGHTCOL_ENCRYPTED];
item = [menu addItemWithTitle:@"Comments" item = [menu addItemWithTitle:@"Comments"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"n"]; keyEquivalent:@""];
[keys applyToMenuItem:item forAction:MAMEDebugActionShowComments];
[item setTarget:self]; [item setTarget:self];
[item setTag:DASM_RIGHTCOL_COMMENTS]; [item setTag:DASM_RIGHTCOL_COMMENTS];
} }
@ -208,52 +214,57 @@
- (void)insertActionItemsInMenu:(NSMenu *)menu atIndex:(NSInteger)index { - (void)insertActionItemsInMenu:(NSMenu *)menu atIndex:(NSInteger)index {
MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap];
NSMenuItem *breakItem = [menu insertItemWithTitle:@"Toggle Breakpoint at Cursor" NSMenuItem *breakItem = [menu insertItemWithTitle:@"Toggle Breakpoint at Cursor"
action:@selector(debugToggleBreakpoint:) action:@selector(debugToggleBreakpoint:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey] keyEquivalent:@""
atIndex:index++]; atIndex:index++];
[breakItem setKeyEquivalentModifierMask:0]; [keys applyToMenuItem:breakItem forAction:MAMEDebugActionToggleBreakpoint];
NSMenuItem *disableItem = [menu insertItemWithTitle:@"Disable Breakpoint at Cursor" NSMenuItem *disableItem = [menu insertItemWithTitle:@"Disable Breakpoint at Cursor"
action:@selector(debugToggleBreakpointEnable:) action:@selector(debugToggleBreakpointEnable:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey] keyEquivalent:@""
atIndex:index++]; atIndex:index++];
[disableItem setKeyEquivalentModifierMask:NSEventModifierFlagShift]; [keys applyToMenuItem:disableItem forAction:MAMEDebugActionDisableBreakpoint];
NSMenu *runMenu = [[menu itemWithTitle:@"Run"] submenu]; NSMenu *runMenu = [[menu itemWithTitle:@"Run"] submenu];
NSMenuItem *runItem; NSMenuItem *runItem;
if (runMenu != nil) { if (runMenu != nil) {
runItem = [runMenu addItemWithTitle:@"to Cursor" runItem = [runMenu addItemWithTitle:@"to Cursor"
action:@selector(debugRunToCursor:) action:@selector(debugRunToCursor:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]]; keyEquivalent:@""];
} else { } else {
runItem = [menu insertItemWithTitle:@"Run to Cursor" runItem = [menu insertItemWithTitle:@"Run to Cursor"
action:@selector(debugRunToCursor:) action:@selector(debugRunToCursor:)
keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey] keyEquivalent:@""
atIndex:index++]; atIndex:index++];
} }
[runItem setKeyEquivalentModifierMask:0]; [keys applyToMenuItem:runItem forAction:MAMEDebugActionRunToCursor];
[menu insertItem:[NSMenuItem separatorItem] atIndex:index++]; [menu insertItem:[NSMenuItem separatorItem] atIndex:index++];
NSMenuItem *rawItem = [menu insertItemWithTitle:@"Show Raw Opcodes" NSMenuItem *rawItem = [menu insertItemWithTitle:@"Show Raw Opcodes"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"r" keyEquivalent:@""
atIndex:index++]; atIndex:index++];
[keys applyToMenuItem:rawItem forAction:MAMEDebugActionShowRawOpcodes];
[rawItem setTarget:self]; [rawItem setTarget:self];
[rawItem setTag:DASM_RIGHTCOL_RAW]; [rawItem setTag:DASM_RIGHTCOL_RAW];
NSMenuItem *encItem = [menu insertItemWithTitle:@"Show Encrypted Opcodes" NSMenuItem *encItem = [menu insertItemWithTitle:@"Show Encrypted Opcodes"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"e" keyEquivalent:@""
atIndex:index++]; atIndex:index++];
[keys applyToMenuItem:encItem forAction:MAMEDebugActionShowEncryptedOpcodes];
[encItem setTarget:self]; [encItem setTarget:self];
[encItem setTag:DASM_RIGHTCOL_ENCRYPTED]; [encItem setTag:DASM_RIGHTCOL_ENCRYPTED];
NSMenuItem *commentsItem = [menu insertItemWithTitle:@"Show Comments" NSMenuItem *commentsItem = [menu insertItemWithTitle:@"Show Comments"
action:@selector(showRightColumn:) action:@selector(showRightColumn:)
keyEquivalent:@"n" keyEquivalent:@""
atIndex:index++]; atIndex:index++];
[keys applyToMenuItem:commentsItem forAction:MAMEDebugActionShowComments];
[commentsItem setTarget:self]; [commentsItem setTarget:self];
[commentsItem setTag:DASM_RIGHTCOL_COMMENTS]; [commentsItem setTag:DASM_RIGHTCOL_COMMENTS];

View File

@ -87,9 +87,12 @@ DasmWindow::DasmWindow(DebuggerQt &debugger, QWidget *parent) :
m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this); m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this);
m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this); m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this);
m_runToCursorAct = new QAction("Run to Cursor", this); m_runToCursorAct = new QAction("Run to Cursor", this);
m_breakpointToggleAct->setShortcut(Qt::Key_F9); //m_breakpointToggleAct->setShortcut(Qt::Key_F9);
m_breakpointEnableAct->setShortcut(0 | Qt::SHIFT | Qt::Key_F9); // zero because C++20 doesn't allow arithmetic between different enums m_breakpointToggleAct->setShortcut(Qt::CTRL | Qt::Key_F18);
m_runToCursorAct->setShortcut(Qt::Key_F4); //m_breakpointEnableAct->setShortcut(0 | Qt::SHIFT | Qt::Key_F9); // zero because C++20 doesn't allow arithmetic between different enums
m_breakpointEnableAct->setShortcut(0 | Qt::SHIFT | Qt::Key_F18); // zero because C++20 doesn't allow arithmetic between different enums
//m_runToCursorAct->setShortcut(Qt::Key_F4);
m_runToCursorAct->setShortcut(Qt::Key_F18);
connect(m_breakpointToggleAct, &QAction::triggered, this, &DasmWindow::toggleBreakpointAtCursor); connect(m_breakpointToggleAct, &QAction::triggered, this, &DasmWindow::toggleBreakpointAtCursor);
connect(m_breakpointEnableAct, &QAction::triggered, this, &DasmWindow::enableBreakpointAtCursor); connect(m_breakpointEnableAct, &QAction::triggered, this, &DasmWindow::enableBreakpointAtCursor);
connect(m_runToCursorAct, &QAction::triggered, this, &DasmWindow::runToCursor); connect(m_runToCursorAct, &QAction::triggered, this, &DasmWindow::runToCursor);
@ -109,9 +112,9 @@ DasmWindow::DasmWindow(DebuggerQt &debugger, QWidget *parent) :
rightActRaw->setActionGroup(rightBarGroup); rightActRaw->setActionGroup(rightBarGroup);
rightActEncrypted->setActionGroup(rightBarGroup); rightActEncrypted->setActionGroup(rightBarGroup);
rightActComments->setActionGroup(rightBarGroup); rightActComments->setActionGroup(rightBarGroup);
rightActRaw->setShortcut(QKeySequence("Ctrl+R")); //rightActRaw->setShortcut(QKeySequence("Ctrl+R"));
rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); //rightActEncrypted->setShortcut(QKeySequence("Ctrl+E"));
rightActComments->setShortcut(QKeySequence("Ctrl+N")); //rightActComments->setShortcut(QKeySequence("Ctrl+N"));
rightActRaw->setChecked(true); rightActRaw->setChecked(true);
connect(rightBarGroup, &QActionGroup::triggered, this, &DasmWindow::rightBarChanged); connect(rightBarGroup, &QActionGroup::triggered, this, &DasmWindow::rightBarChanged);

View File

@ -73,10 +73,14 @@ void DebuggerView::paintEvent(QPaintEvent *event)
QFontMetrics actualFont = fontMetrics(); QFontMetrics actualFont = fontMetrics();
double const fontWidth = actualFont.horizontalAdvance(QString(100, '_')) / 100.; double const fontWidth = actualFont.horizontalAdvance(QString(100, '_')) / 100.;
int const fontHeight = std::max(1, actualFont.lineSpacing()); int const fontHeight = std::max(1, actualFont.lineSpacing());
int const contentWidth = width() - verticalScrollBar()->width(); // Use the scroll bars' size hints rather than their current width()/height():
// before the scroll area has laid out its scroll bars (i.e. on the very first
// paint) the bars still report the default QWidget size (100px), which would
// collapse contentWidth and hide the right-hand columns until a manual resize.
int const contentWidth = width() - verticalScrollBar()->sizeHint().width();
int const lineWidth = contentWidth / fontWidth; int const lineWidth = contentWidth / fontWidth;
bool const fullWidth = lineWidth >= m_view->total_size().x; bool const fullWidth = lineWidth >= m_view->total_size().x;
int const contentHeight = height() - (fullWidth ? 0 : horizontalScrollBar()->height()); int const contentHeight = height() - (fullWidth ? 0 : horizontalScrollBar()->sizeHint().height());
m_view->set_visible_size(debug_view_xy(lineWidth, contentHeight / fontHeight)); m_view->set_visible_size(debug_view_xy(lineWidth, contentHeight / fontHeight));
// Handle the scroll bars // Handle the scroll bars
@ -126,7 +130,8 @@ void DebuggerView::paintEvent(QPaintEvent *event)
bgColor.setRgb(palette.color(QPalette::Base).rgb()); bgColor.setRgb(palette.color(QPalette::Base).rgb());
if (textAttr & DCA_SELECTED) if (textAttr & DCA_SELECTED)
bgColor.setRgb(0xcb, 0x4b, 0x16); //bgColor.setRgb(0xcb, 0x4b, 0x16);
bgColor.setRgb(0x30, 0x80, 0x80);
if (textAttr & DCA_CURRENT) if (textAttr & DCA_CURRENT)
bgColor.setRgb(palette.color(QPalette::Highlight).rgb()); bgColor.setRgb(palette.color(QPalette::Highlight).rgb());

View File

@ -62,9 +62,11 @@ MainWindow::MainWindow(DebuggerQt &debugger, QWidget *parent) :
m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this); m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this);
m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this); m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this);
m_runToCursorAct = new QAction("Run to Cursor", this); m_runToCursorAct = new QAction("Run to Cursor", this);
m_breakpointToggleAct->setShortcut(Qt::Key_F9); //m_breakpointToggleAct->setShortcut(Qt::Key_F9);
m_breakpointEnableAct->setShortcut(0 | Qt::SHIFT | Qt::Key_F9); // zero because C++20 doesn't allow arithmetic between different enums m_breakpointToggleAct->setShortcut(0 | Qt::CTRL | Qt::Key_F18);
m_runToCursorAct->setShortcut(Qt::Key_F4); //m_breakpointEnableAct->setShortcut(0 | Qt::SHIFT | Qt::Key_F9); // zero because C++20 doesn't allow arithmetic between different enums
//m_runToCursorAct->setShortcut(Qt::Key_F4);
m_runToCursorAct->setShortcut(Qt::Key_F18);
connect(m_breakpointToggleAct, &QAction::triggered, this, &MainWindow::toggleBreakpointAtCursor); connect(m_breakpointToggleAct, &QAction::triggered, this, &MainWindow::toggleBreakpointAtCursor);
connect(m_breakpointEnableAct, &QAction::triggered, this, &MainWindow::enableBreakpointAtCursor); connect(m_breakpointEnableAct, &QAction::triggered, this, &MainWindow::enableBreakpointAtCursor);
connect(m_runToCursorAct, &QAction::triggered, this, &MainWindow::runToCursor); connect(m_runToCursorAct, &QAction::triggered, this, &MainWindow::runToCursor);
@ -84,9 +86,9 @@ MainWindow::MainWindow(DebuggerQt &debugger, QWidget *parent) :
rightActRaw->setActionGroup(rightBarGroup); rightActRaw->setActionGroup(rightBarGroup);
rightActEncrypted->setActionGroup(rightBarGroup); rightActEncrypted->setActionGroup(rightBarGroup);
rightActComments->setActionGroup(rightBarGroup); rightActComments->setActionGroup(rightBarGroup);
rightActRaw->setShortcut(QKeySequence("Ctrl+R")); //rightActRaw->setShortcut(QKeySequence("Ctrl+R"));
rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); //rightActEncrypted->setShortcut(QKeySequence("Ctrl+E"));
rightActComments->setShortcut(QKeySequence("Ctrl+N")); //rightActComments->setShortcut(QKeySequence("Ctrl+N"));
rightActRaw->setChecked(true); rightActRaw->setChecked(true);
connect(rightBarGroup, &QActionGroup::triggered, this, &MainWindow::rightBarChanged); connect(rightBarGroup, &QActionGroup::triggered, this, &MainWindow::rightBarChanged);

View File

@ -15,12 +15,78 @@
#include "util/xmlfile.h" #include "util/xmlfile.h"
#include <QtWidgets/QDialog>
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QKeySequenceEdit>
#include <QtWidgets/QLabel>
#include <QtWidgets/QMenu> #include <QtWidgets/QMenu>
#include <QtWidgets/QMenuBar> #include <QtWidgets/QMenuBar>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QScrollArea>
#include <QtWidgets/QVBoxLayout>
#include <vector>
namespace osd::debugger::qt { namespace osd::debugger::qt {
namespace {
// action identifiers (also used as keys in the configuration file)
constexpr char const *ACT_NEW_MEMORY = "new_memory";
constexpr char const *ACT_NEW_DISASM = "new_disasm";
constexpr char const *ACT_NEW_LOG = "new_log";
constexpr char const *ACT_NEW_POINTS = "new_points";
constexpr char const *ACT_NEW_DEVICES = "new_devices";
constexpr char const *ACT_RUN = "run";
constexpr char const *ACT_RUN_AND_HIDE = "run_and_hide";
constexpr char const *ACT_RUN_NEXT_CPU = "run_next_cpu";
constexpr char const *ACT_RUN_NEXT_INT = "run_next_int";
constexpr char const *ACT_RUN_VBLANK = "run_vblank";
constexpr char const *ACT_STEP_INTO = "step_into";
constexpr char const *ACT_STEP_OVER = "step_over";
constexpr char const *ACT_STEP_OUT = "step_out";
constexpr char const *ACT_SOFT_RESET = "soft_reset";
constexpr char const *ACT_HARD_RESET = "hard_reset";
constexpr char const *ACT_CLOSE_WINDOW = "close_window";
constexpr char const *ACT_QUIT = "quit";
osd::debugger::key_shortcut make_sc(char const *key, bool ctrl, bool shift)
{
osd::debugger::key_shortcut sc;
sc.key = key;
sc.ctrl = ctrl;
sc.shift = shift;
return sc;
}
} // anonymous namespace
std::vector<osd::debugger::key_action> qtDefaultKeyActions()
{
return {
{ ACT_NEW_MEMORY, "New Memory Window", "Windows", make_sc("M", true, false) },
{ ACT_NEW_DISASM, "New Disassembly Window", "Windows", make_sc("D", true, false) },
{ ACT_NEW_LOG, "New Error Log Window", "Windows", make_sc("L", true, false) },
{ ACT_NEW_POINTS, "New (Break|Watch)points Window", "Windows", make_sc("B", true, false) },
{ ACT_NEW_DEVICES, "New Devices Window", "Windows", osd::debugger::key_shortcut() },
{ ACT_RUN, "Run / Break", "Execution", make_sc("F5", false, false) },
{ ACT_RUN_AND_HIDE, "Run and Hide Debugger", "Execution", make_sc("F12", false, false) },
{ ACT_RUN_NEXT_CPU, "Run to Next CPU", "Execution", make_sc("F6", false, false) },
{ ACT_RUN_NEXT_INT, "Run to Next Interrupt", "Execution", make_sc("F7", false, false) },
{ ACT_RUN_VBLANK, "Run to Next VBLANK", "Execution", make_sc("F8", false, false) },
{ ACT_STEP_INTO, "Step Into", "Execution", make_sc("F11", false, false) },
{ ACT_STEP_OVER, "Step Over", "Execution", make_sc("F10", false, false) },
{ ACT_STEP_OUT, "Step Out", "Execution", make_sc("F11", false, true) },
{ ACT_SOFT_RESET, "Soft Reset", "Execution", make_sc("F3", false, false) },
{ ACT_HARD_RESET, "Hard Reset", "Execution", make_sc("F3", false, true) },
{ ACT_CLOSE_WINDOW, "Close Window", "Windows", make_sc("W", true, false) },
{ ACT_QUIT, "Quit", "Windows", make_sc("Q", true, false) }
};
}
// Since all debug windows are intended to be top-level, this inherited // Since all debug windows are intended to be top-level, this inherited
// constructor is always called with a nullptr parent. The passed-in parent widget, // constructor is always called with a nullptr parent. The passed-in parent widget,
// however, is often used to place each child window & the code to do this can // however, is often used to place each child window & the code to do this can
@ -38,74 +104,29 @@ WindowQt::WindowQt(DebuggerQt &debugger, QWidget *parent) :
connect(&debugger, &DebuggerQt::hideAllWindows, this, &WindowQt::hide); connect(&debugger, &DebuggerQt::hideAllWindows, this, &WindowQt::hide);
connect(&debugger, &DebuggerQt::showAllWindows, this, &WindowQt::show); connect(&debugger, &DebuggerQt::showAllWindows, this, &WindowQt::show);
connect(&debugger, &DebuggerQt::saveConfiguration, this, &WindowQt::saveConfiguration); connect(&debugger, &DebuggerQt::saveConfiguration, this, &WindowQt::saveConfiguration);
connect(&debugger, &DebuggerQt::keyBindingsChanged, this, &WindowQt::applyKeyBindings);
// The Debug menu bar // The Debug menu bar - shortcuts come from the remappable key map
QAction *debugActOpenMemory = new QAction("New &Memory Window", this); QAction *debugActOpenMemory = createKeyAction(ACT_NEW_MEMORY, "New &Memory Window", &WindowQt::debugActOpenMemory);
debugActOpenMemory->setShortcut(QKeySequence("Ctrl+M")); QAction *debugActOpenDasm = createKeyAction(ACT_NEW_DISASM, "New &Disassembly Window", &WindowQt::debugActOpenDasm);
connect(debugActOpenMemory, &QAction::triggered, this, &WindowQt::debugActOpenMemory); QAction *debugActOpenLog = createKeyAction(ACT_NEW_LOG, "New Error &Log Window", &WindowQt::debugActOpenLog);
QAction *debugActOpenPoints = createKeyAction(ACT_NEW_POINTS, "New (&Break|Watch)points Window", &WindowQt::debugActOpenPoints);
QAction *debugActOpenDevices = createKeyAction(ACT_NEW_DEVICES, "New D&evices Window", &WindowQt::debugActOpenDevices);
QAction *dbgActRun = createKeyAction(ACT_RUN, "Run/Break", &WindowQt::debugActRun);
QAction *dbgActRunAndHide = createKeyAction(ACT_RUN_AND_HIDE, "Run And Hide Debugger", &WindowQt::debugActRunAndHide);
QAction *dbgActRunToNextCpu = createKeyAction(ACT_RUN_NEXT_CPU, "Run to Next CPU", &WindowQt::debugActRunToNextCpu);
QAction *dbgActRunNextInt = createKeyAction(ACT_RUN_NEXT_INT, "Run to Next Interrupt on This CPU", &WindowQt::debugActRunNextInt);
QAction *dbgActRunNextVBlank = createKeyAction(ACT_RUN_VBLANK, "Run to Next VBlank", &WindowQt::debugActRunNextVBlank);
QAction *dbgActStepInto = createKeyAction(ACT_STEP_INTO, "Step Into", &WindowQt::debugActStepInto);
QAction *dbgActStepOver = createKeyAction(ACT_STEP_OVER, "Step Over", &WindowQt::debugActStepOver);
QAction *dbgActStepOut = createKeyAction(ACT_STEP_OUT, "Step Out", &WindowQt::debugActStepOut);
QAction *dbgActSoftReset = createKeyAction(ACT_SOFT_RESET, "Soft Reset", &WindowQt::debugActSoftReset);
QAction *dbgActHardReset = createKeyAction(ACT_HARD_RESET, "Hard Reset", &WindowQt::debugActHardReset);
QAction *dbgActClose = createKeyAction(ACT_CLOSE_WINDOW, "Close &Window", &WindowQt::debugActClose);
QAction *dbgActQuit = createKeyAction(ACT_QUIT, "&Quit", &WindowQt::debugActQuit);
QAction *debugActOpenDasm = new QAction("New &Disassembly Window", this); QAction *dbgActCustomizeKeys = new QAction("Customize &Keys...", this);
debugActOpenDasm->setShortcut(QKeySequence("Ctrl+D")); connect(dbgActCustomizeKeys, &QAction::triggered, this, &WindowQt::debugActCustomizeKeys);
connect(debugActOpenDasm, &QAction::triggered, this, &WindowQt::debugActOpenDasm);
QAction *debugActOpenLog = new QAction("New Error &Log Window", this);
debugActOpenLog->setShortcut(QKeySequence("Ctrl+L"));
connect(debugActOpenLog, &QAction::triggered, this, &WindowQt::debugActOpenLog);
QAction *debugActOpenPoints = new QAction("New (&Break|Watch)points Window", this);
debugActOpenPoints->setShortcut(QKeySequence("Ctrl+B"));
connect(debugActOpenPoints, &QAction::triggered, this, &WindowQt::debugActOpenPoints);
QAction *debugActOpenDevices = new QAction("New D&evices Window", this);
connect(debugActOpenDevices, &QAction::triggered, this, &WindowQt::debugActOpenDevices);
QAction *dbgActRun = new QAction("Run", this);
dbgActRun->setShortcut(Qt::Key_F5);
connect(dbgActRun, &QAction::triggered, this, &WindowQt::debugActRun);
QAction *dbgActRunAndHide = new QAction("Run And Hide Debugger", this);
dbgActRunAndHide->setShortcut(Qt::Key_F12);
connect(dbgActRunAndHide, &QAction::triggered, this, &WindowQt::debugActRunAndHide);
QAction *dbgActRunToNextCpu = new QAction("Run to Next CPU", this);
dbgActRunToNextCpu->setShortcut(Qt::Key_F6);
connect(dbgActRunToNextCpu, &QAction::triggered, this, &WindowQt::debugActRunToNextCpu);
QAction *dbgActRunNextInt = new QAction("Run to Next Interrupt on This CPU", this);
dbgActRunNextInt->setShortcut(Qt::Key_F7);
connect(dbgActRunNextInt, &QAction::triggered, this, &WindowQt::debugActRunNextInt);
QAction *dbgActRunNextVBlank = new QAction("Run to Next VBlank", this);
dbgActRunNextVBlank->setShortcut(Qt::Key_F8);
connect(dbgActRunNextVBlank, &QAction::triggered, this, &WindowQt::debugActRunNextVBlank);
QAction *dbgActStepInto = new QAction("Step Into", this);
dbgActStepInto->setShortcut(Qt::Key_F11);
connect(dbgActStepInto, &QAction::triggered, this, &WindowQt::debugActStepInto);
QAction *dbgActStepOver = new QAction("Step Over", this);
dbgActStepOver->setShortcut(Qt::Key_F10);
connect(dbgActStepOver, &QAction::triggered, this, &WindowQt::debugActStepOver);
QAction *dbgActStepOut = new QAction("Step Out", this);
dbgActStepOut->setShortcut(QKeySequence("Shift+F11"));
connect(dbgActStepOut, &QAction::triggered, this, &WindowQt::debugActStepOut);
QAction *dbgActSoftReset = new QAction("Soft Reset", this);
dbgActSoftReset->setShortcut(Qt::Key_F3);
connect(dbgActSoftReset, &QAction::triggered, this, &WindowQt::debugActSoftReset);
QAction *dbgActHardReset = new QAction("Hard Reset", this);
dbgActHardReset->setShortcut(QKeySequence("Shift+F3"));
connect(dbgActHardReset, &QAction::triggered, this, &WindowQt::debugActHardReset);
QAction *dbgActClose = new QAction("Close &Window", this);
dbgActClose->setShortcut(QKeySequence::Close);
connect(dbgActClose, &QAction::triggered, this, &WindowQt::debugActClose);
QAction *dbgActQuit = new QAction("&Quit", this);
dbgActQuit->setShortcut(QKeySequence::Quit);
connect(dbgActQuit, &QAction::triggered, this, &WindowQt::debugActQuit);
// Construct the menu // Construct the menu
QMenu *debugMenu = menuBar()->addMenu("&Debug"); QMenu *debugMenu = menuBar()->addMenu("&Debug");
@ -128,11 +149,30 @@ WindowQt::WindowQt(DebuggerQt &debugger, QWidget *parent) :
debugMenu->addAction(dbgActSoftReset); debugMenu->addAction(dbgActSoftReset);
debugMenu->addAction(dbgActHardReset); debugMenu->addAction(dbgActHardReset);
debugMenu->addSeparator(); debugMenu->addSeparator();
debugMenu->addAction(dbgActCustomizeKeys);
debugMenu->addSeparator();
debugMenu->addAction(dbgActClose); debugMenu->addAction(dbgActClose);
debugMenu->addAction(dbgActQuit); debugMenu->addAction(dbgActQuit);
} }
QAction *WindowQt::createKeyAction(char const *id, QString const &text, void (WindowQt::*slot)())
{
QAction *const action = new QAction(text, this);
connect(action, &QAction::triggered, this, slot);
m_keyActions[id] = action;
action->setShortcut(QKeySequence(QString::fromStdString(m_debugger.keymap().shortcut(id).to_string())));
return action;
}
void WindowQt::applyKeyBindings()
{
for (auto const &entry : m_keyActions)
entry.second->setShortcut(QKeySequence(QString::fromStdString(m_debugger.keymap().shortcut(entry.first).to_string())));
}
WindowQt::~WindowQt() WindowQt::~WindowQt()
{ {
} }
@ -250,6 +290,81 @@ void WindowQt::debugActQuit()
m_machine.schedule_exit(); m_machine.schedule_exit();
} }
void WindowQt::debugActCustomizeKeys()
{
osd::debugger::keymap_config &keymap = m_debugger.keymap();
QDialog dialog(this);
dialog.setWindowTitle("Customize Debugger Keys");
dialog.resize(440, 480);
QVBoxLayout *const outer = new QVBoxLayout(&dialog);
QLabel *const hint = new QLabel(
"Click a field and press the desired key combination. Use Backspace to clear a binding.",
&dialog);
hint->setWordWrap(true);
outer->addWidget(hint);
QScrollArea *const scroll = new QScrollArea(&dialog);
scroll->setWidgetResizable(true);
QWidget *const content = new QWidget(scroll);
QGridLayout *const grid = new QGridLayout(content);
std::vector<std::pair<std::string, QKeySequenceEdit *> > edits;
int row = 0;
std::string group;
for (osd::debugger::key_action const &action : keymap.actions())
{
if (action.group != group)
{
group = action.group;
QLabel *const header = new QLabel(QString("<b>%1</b>").arg(QString::fromStdString(group)), content);
grid->addWidget(header, row++, 0, 1, 2);
}
QLabel *const label = new QLabel(QString::fromStdString(action.label), content);
QKeySequenceEdit *const edit = new QKeySequenceEdit(
QKeySequence(QString::fromStdString(keymap.shortcut(action.id).to_string())),
content);
grid->addWidget(label, row, 0);
grid->addWidget(edit, row, 1);
edits.emplace_back(action.id, edit);
++row;
}
scroll->setWidget(content);
outer->addWidget(scroll, 1);
QDialogButtonBox *const buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults,
&dialog);
outer->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(buttons->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, &dialog,
[&edits, &keymap] ()
{
for (auto const &entry : edits)
{
keymap.reset(entry.first);
entry.second->setKeySequence(QKeySequence(QString::fromStdString(keymap.shortcut(entry.first).to_string())));
}
});
if (dialog.exec() == QDialog::Accepted)
{
for (auto const &entry : edits)
{
// keep only the first chord and store it in portable form
QString portable = entry.second->keySequence().toString(QKeySequence::PortableText);
int const comma = portable.indexOf(", ");
if (comma >= 0)
portable = portable.left(comma);
keymap.set_shortcut(entry.first, osd::debugger::key_shortcut::from_string(portable.toStdString()));
}
m_debugger.notifyKeyBindingsChanged();
}
}
void WindowQt::debuggerExit() void WindowQt::debuggerExit()
{ {
// this isn't called from a Qt event loop, so close() will leak the window object // this isn't called from a Qt event loop, so close() will leak the window object

View File

@ -4,6 +4,7 @@
#define MAME_DEBUGGER_QT_WINDOWQT_H #define MAME_DEBUGGER_QT_WINDOWQT_H
#include "../xmlconfig.h" #include "../xmlconfig.h"
#include "../debugkeyconfig.h"
#ifdef __aarch64__ #ifdef __aarch64__
#include <arm_acle.h> // QtCore/qyieldcpu.h uses __yield() without #including this, causing an error #include <arm_acle.h> // QtCore/qyieldcpu.h uses __yield() without #including this, causing an error
@ -12,11 +13,18 @@
#include <QtWidgets/QMainWindow> #include <QtWidgets/QMainWindow>
#include <deque> #include <deque>
#include <map>
#include <memory> #include <memory>
#include <vector>
class QAction;
namespace osd::debugger::qt { namespace osd::debugger::qt {
// table of remappable actions with their default Qt shortcuts (defined in windowqt.cpp)
std::vector<osd::debugger::key_action> qtDefaultKeyActions();
//============================================================ //============================================================
// The Qt debugger module interface // The Qt debugger module interface
//============================================================ //============================================================
@ -29,13 +37,18 @@ public:
virtual running_machine &machine() const = 0; virtual running_machine &machine() const = 0;
// shared, remappable keyboard shortcut map
virtual osd::debugger::keymap_config &keymap() = 0;
void hideAll() { emit hideAllWindows(); } void hideAll() { emit hideAllWindows(); }
void notifyKeyBindingsChanged() { emit keyBindingsChanged(); }
signals: signals:
void exitDebugger(); void exitDebugger();
void hideAllWindows(); void hideAllWindows();
void showAllWindows(); void showAllWindows();
void saveConfiguration(util::xml::data_node &parentnode); void saveConfiguration(util::xml::data_node &parentnode);
void keyBindingsChanged();
}; };
@ -69,18 +82,24 @@ protected slots:
void debugActHardReset(); void debugActHardReset();
virtual void debugActClose(); virtual void debugActClose();
void debugActQuit(); void debugActQuit();
void debugActCustomizeKeys();
virtual void debuggerExit(); virtual void debuggerExit();
private slots: private slots:
void saveConfiguration(util::xml::data_node &parentnode); void saveConfiguration(util::xml::data_node &parentnode);
void applyKeyBindings();
protected: protected:
WindowQt(DebuggerQt &debugger, QWidget *parent = nullptr); WindowQt(DebuggerQt &debugger, QWidget *parent = nullptr);
virtual void saveConfigurationToNode(util::xml::data_node &node); virtual void saveConfigurationToNode(util::xml::data_node &node);
// create a menu action whose shortcut is driven by the remappable key map
QAction *createKeyAction(char const *id, QString const &text, void (WindowQt::*slot)());
DebuggerQt &m_debugger; DebuggerQt &m_debugger;
running_machine &m_machine; running_machine &m_machine;
std::map<std::string, QAction *> m_keyActions; // action id -> menu action, for live shortcut updates
}; };

View File

@ -7,6 +7,9 @@ namespace osd::debugger {
char const *const NODE_WINDOW = "window"; char const *const NODE_WINDOW = "window";
char const *const NODE_COLORS = "colors"; char const *const NODE_COLORS = "colors";
char const *const NODE_KEYMAP = "keymap";
char const *const NODE_KEYMAP_ITEM = "key";
char const *const NODE_WINDOW_SPLITS = "splits"; char const *const NODE_WINDOW_SPLITS = "splits";
char const *const NODE_WINDOW_SELECTION = "selection"; char const *const NODE_WINDOW_SELECTION = "selection";
@ -41,6 +44,10 @@ char const *const ATTR_WINDOW_DEVICE_TAG = "device-tag";
char const *const ATTR_COLORS_THEME = "theme"; char const *const ATTR_COLORS_THEME = "theme";
char const *const ATTR_KEYMAP_ACTION = "action";
char const *const ATTR_KEYMAP_KEY = "char";
char const *const ATTR_KEYMAP_MODIFIERS = "modifiers";
char const *const ATTR_SPLITS_CONSOLE_STATE = "state"; char const *const ATTR_SPLITS_CONSOLE_STATE = "state";
char const *const ATTR_SPLITS_CONSOLE_DISASSEMBLY = "disassembly"; char const *const ATTR_SPLITS_CONSOLE_DISASSEMBLY = "disassembly";

View File

@ -23,6 +23,9 @@ WINDOW_TYPE_DEVICE_INFO_VIEWER
extern char const *const NODE_WINDOW; extern char const *const NODE_WINDOW;
extern char const *const NODE_COLORS; extern char const *const NODE_COLORS;
extern char const *const NODE_KEYMAP;
extern char const *const NODE_KEYMAP_ITEM;
extern char const *const NODE_WINDOW_SPLITS; extern char const *const NODE_WINDOW_SPLITS;
extern char const *const NODE_WINDOW_SELECTION; extern char const *const NODE_WINDOW_SELECTION;
@ -57,6 +60,10 @@ extern char const *const ATTR_WINDOW_DEVICE_TAG;
extern char const *const ATTR_COLORS_THEME; extern char const *const ATTR_COLORS_THEME;
extern char const *const ATTR_KEYMAP_ACTION;
extern char const *const ATTR_KEYMAP_KEY;
extern char const *const ATTR_KEYMAP_MODIFIERS;
extern char const *const ATTR_SPLITS_CONSOLE_STATE; extern char const *const ATTR_SPLITS_CONSOLE_STATE;
extern char const *const ATTR_SPLITS_CONSOLE_DISASSEMBLY; extern char const *const ATTR_SPLITS_CONSOLE_DISASSEMBLY;

View File

@ -331,6 +331,12 @@ protected:
virtual bool should_poll_devices() virtual bool should_poll_devices()
{ {
// when the user freed the pointer to the OS (UI Release Pointer), stop
// feeding input to the emulated machine; poll() then resets the devices
// each frame, so nothing sticks and no events back up
if (osd().pointer_released())
return false;
return background_input() || osd().has_focus(); return background_input() || osd().has_focus();
} }

View File

@ -25,6 +25,10 @@
bool windows_osd_interface::should_hide_mouse() const bool windows_osd_interface::should_hide_mouse() const
{ {
// if the user toggled the pointer free, no
if (m_pointer_released)
return false;
if (!winwindow_has_focus()) if (!winwindow_has_focus())
return false; return false;

View File

@ -283,6 +283,19 @@ protected:
void poll_input_modules(bool relative_reset); void poll_input_modules(bool relative_reset);
// user toggled "UI Release Pointer" - when set, forces the pointer to be released to the OS
bool m_pointer_released = false;
void toggle_pointer_release() { m_pointer_released = !m_pointer_released; }
public:
// when true, the user freed the pointer to the OS - input modules suspend
// feeding mouse/keyboard to the emulated machine while this is set
bool pointer_released() const { return m_pointer_released; }
// re-capture the pointer (e.g. user clicked back in the window)
void recapture_pointer() { m_pointer_released = false; }
protected:
static std::list<std::unique_ptr<osd_window> > s_window_list; static std::list<std::unique_ptr<osd_window> > s_window_list;
private: private:

View File

@ -448,6 +448,10 @@ void sdl_osd_interface::release_keys()
bool sdl_osd_interface::should_hide_mouse() bool sdl_osd_interface::should_hide_mouse()
{ {
// if the user toggled the pointer free, no
if (m_pointer_released)
return false;
// if we are paused, no // if we are paused, no
if (machine().paused()) if (machine().paused())
return false; return false;
@ -819,6 +823,10 @@ void sdl_osd_interface::check_osd_inputs()
if (machine().ui_input().pressed(IPT_OSD_8)) if (machine().ui_input().pressed(IPT_OSD_8))
window->renderer().record(); window->renderer().record();
// toggle releasing the pointer back to the OS
if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER))
toggle_pointer_release();
} }

View File

@ -285,7 +285,15 @@ void sdl_window_info::update_cursor_state()
// the possibility of losing control // the possibility of losing control
if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED)) if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED))
{ {
bool should_hide_mouse = downcast<sdl_osd_interface&>(machine().osd()).should_hide_mouse(); auto &sdlosd = downcast<sdl_osd_interface&>(machine().osd());
// if the pointer was freed to the OS, re-capture when the user clicks back in the window
if (sdlosd.pointer_released()
&& (SDL_GetMouseFocus() == platform_window())
&& (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)))
sdlosd.recapture_pointer();
bool should_hide_mouse = sdlosd.should_hide_mouse();
if (!fullscreen() && !should_hide_mouse) if (!fullscreen() && !should_hide_mouse)
{ {

View File

@ -462,6 +462,10 @@ void sdl_osd_interface::release_keys()
bool sdl_osd_interface::should_hide_mouse() bool sdl_osd_interface::should_hide_mouse()
{ {
// if the user toggled the pointer free, no
if (m_pointer_released)
return false;
// if we are paused, no // if we are paused, no
if (machine().paused()) if (machine().paused())
return false; return false;
@ -860,6 +864,10 @@ void sdl_osd_interface::check_osd_inputs()
if (machine().ui_input().pressed(IPT_OSD_8)) if (machine().ui_input().pressed(IPT_OSD_8))
window->renderer().record(); window->renderer().record();
// toggle releasing the pointer back to the OS
if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER))
toggle_pointer_release();
} }

View File

@ -284,7 +284,15 @@ void sdl_window_info::update_cursor_state()
// the possibility of losing control // the possibility of losing control
if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED)) if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED))
{ {
bool should_hide_mouse = downcast<sdl_osd_interface&>(machine().osd()).should_hide_mouse(); auto &sdlosd = downcast<sdl_osd_interface&>(machine().osd());
// if the pointer was freed to the OS, re-capture when the user clicks back in the window
if (sdlosd.pointer_released()
&& (SDL_GetMouseFocus() == platform_window())
&& (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)))
sdlosd.recapture_pointer();
bool should_hide_mouse = sdlosd.should_hide_mouse();
if (!fullscreen() && !should_hide_mouse) if (!fullscreen() && !should_hide_mouse)
{ {

View File

@ -142,6 +142,10 @@ void windows_osd_interface::check_osd_inputs()
// check for taking fullscreen video // check for taking fullscreen video
if (machine().ui_input().pressed(IPT_OSD_4)) if (machine().ui_input().pressed(IPT_OSD_4))
winwindow_toggle_fsfx(); winwindow_toggle_fsfx();
// toggle releasing the pointer back to the OS
if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER))
toggle_pointer_release();
} }

View File

@ -752,6 +752,12 @@ void winwindow_update_cursor_state(running_machine &machine)
auto &window = static_cast<win_window_info &>(*osd_common_t::window_list().front()); auto &window = static_cast<win_window_info &>(*osd_common_t::window_list().front());
// if the pointer was freed to the OS, re-capture when the user clicks back in the window
if (WINOSD(machine)->pointer_released()
&& winwindow_has_focus()
&& (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
WINOSD(machine)->recapture_pointer();
// if we should hide the mouse cursor, then do it // if we should hide the mouse cursor, then do it
// rules are: // rules are:
// 1. we must have focus before hiding the cursor // 1. we must have focus before hiding the cursor