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