From 6b6a5b0f9cc2d2b139f899beb9fc895e9dd89eb9 Mon Sep 17 00:00:00 2001 From: Tolik <85737314+Tolik-Trek@users.noreply.github.com> Date: Tue, 26 May 2026 22:09:47 +1000 Subject: [PATCH] Vibe changes over upstream MAME (squashed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Содержит правки из следующих коммитов: - 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 --- DEBUGGER_KEYMAP_HANDOFF.md | 170 +++++ MAME_MCP_GUIDE.md | 450 +++++++++++ plugins/mamebridge/init.lua | 696 ++++++++++++++++++ plugins/mamebridge/plugin.json | 10 + plugins/portname/init.lua | 6 +- scripts/src/osd/mac.lua | 4 + scripts/src/osd/modules.lua | 2 + scripts/src/osd/sdl.lua | 4 + scripts/src/osd/sdl3.lua | 4 + src/.claude/settings.json | 15 + src/CLAUDE.md | 205 ++++++ src/devices/bus/abcbus/lux4105.cpp | 504 +++++++++++++ src/devices/bus/abcbus/lux4105.h | 85 +++ src/mame/sinclair/sprinter.cpp | 12 +- src/mame_bridge.lua | 153 ++++ src/mame_mcp.py | 270 +++++++ src/osd/mac/video.cpp | 4 + src/osd/modules/debugger/debugimgui.cpp | 379 +++++++++- src/osd/modules/debugger/debugkeyconfig.cpp | 218 ++++++ src/osd/modules/debugger/debugkeyconfig.h | 104 +++ src/osd/modules/debugger/debugosx.mm | 167 +++-- src/osd/modules/debugger/debugqt.cpp | 20 +- src/osd/modules/debugger/osx/debugkeymap.h | 105 +++ src/osd/modules/debugger/osx/debugkeymap.mm | 391 ++++++++++ .../modules/debugger/osx/debugkeymapviewer.h | 43 ++ .../modules/debugger/osx/debugkeymapviewer.mm | 363 +++++++++ src/osd/modules/debugger/osx/debugview.h | 2 + src/osd/modules/debugger/osx/debugview.mm | 142 ++-- .../modules/debugger/osx/debugwindowhandler.h | 5 +- .../debugger/osx/debugwindowhandler.mm | 160 ++-- .../modules/debugger/osx/disassemblyview.mm | 49 +- src/osd/modules/debugger/qt/dasmwindow.cpp | 15 +- src/osd/modules/debugger/qt/debuggerview.cpp | 11 +- src/osd/modules/debugger/qt/mainwindow.cpp | 14 +- src/osd/modules/debugger/qt/windowqt.cpp | 247 +++++-- src/osd/modules/debugger/qt/windowqt.h | 19 + src/osd/modules/debugger/xmlconfig.cpp | 7 + src/osd/modules/debugger/xmlconfig.h | 7 + src/osd/modules/input/input_common.h | 6 + src/osd/modules/input/input_windows.cpp | 4 + src/osd/modules/lib/osdobj_common.h | 13 + src/osd/sdl/osdsdl.cpp | 8 + src/osd/sdl/window.cpp | 10 +- src/osd/sdl3/osdsdl.cpp | 8 + src/osd/sdl3/window.cpp | 10 +- src/osd/windows/video.cpp | 4 + src/osd/windows/window.cpp | 6 + 47 files changed, 4790 insertions(+), 341 deletions(-) create mode 100644 DEBUGGER_KEYMAP_HANDOFF.md create mode 100644 MAME_MCP_GUIDE.md create mode 100644 plugins/mamebridge/init.lua create mode 100644 plugins/mamebridge/plugin.json create mode 100644 src/.claude/settings.json create mode 100644 src/CLAUDE.md create mode 100644 src/devices/bus/abcbus/lux4105.cpp create mode 100644 src/devices/bus/abcbus/lux4105.h create mode 100644 src/mame_bridge.lua create mode 100644 src/mame_mcp.py create mode 100644 src/osd/modules/debugger/debugkeyconfig.cpp create mode 100644 src/osd/modules/debugger/debugkeyconfig.h create mode 100644 src/osd/modules/debugger/osx/debugkeymap.h create mode 100644 src/osd/modules/debugger/osx/debugkeymap.mm create mode 100644 src/osd/modules/debugger/osx/debugkeymapviewer.h create mode 100644 src/osd/modules/debugger/osx/debugkeymapviewer.mm diff --git a/DEBUGGER_KEYMAP_HANDOFF.md b/DEBUGGER_KEYMAP_HANDOFF.md new file mode 100644 index 000000000..aac194d58 --- /dev/null +++ b/DEBUGGER_KEYMAP_HANDOFF.md @@ -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&& actions)` — хранит таблицу + override’ы: + `shortcut(id)`, `is_default`, `conflicting_action`, `set_shortcut/clear/reset/reset_all`, + `save(node)` / `load(node)` (узел `` с дочерними ``). + +Каждый дебаггер задаёт СВОЮ таблицу действий (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`, +который для позиций окон). Сохраняются только отличия от дефолтов; пустой `` не пишется. + +--- + +## Что и где изменено + +### 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 m_keyActions`. + Объявлена `std::vector 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//...`. +- `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. diff --git a/MAME_MCP_GUIDE.md b/MAME_MCP_GUIDE.md new file mode 100644 index 000000000..e442aeb0e --- /dev/null +++ b/MAME_MCP_GUIDE.md @@ -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_.txt, ждёт resp_.txt в общей папке + ▼ +plugins/mamebridge/init.lua (плагин MAME) + │ читает запрос, исполняет в дебаггере MAME, атомарно пишет ответ + ▼ +MAME debugger ──> эмулируемая машина (Sprinter / любой CPU) +``` + +- **IPC**: текстовые файлы `req_.txt` / `resp_.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@` = 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] [:],,[,[,]]`. + `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 [,[,]]`, `bpclear/bpdisable/bpenable [n,…]`, + `bplist`. +- Шаги/выполнение: `s[tep] [n]`, `o[ver] [n]`, `out`, `g[o] [addr]`, + `gv[blank]`, `gi[nt] [irqline]`, `gt[ime] `, `ge[x] [exc,[cond]]`, + `gbt [cond]` / `gbf [cond]` (взятая/невзятая ветвь), `n[ext]` (до смены CPU), + `focus/ignore/observe `, `trace {file|off}[,cpu[,…]]`, `traceover`, + `traceflush`. +- Общие: `print [,…]` (hex), `printf [,arg…]` (%d %x %X %o %c %s %%), + `logerror`, `history [cpu[,len]]`, `softreset`, `hardreset`, `help [topic]`. + +### 10.3 Выражения и операторы доступа к памяти +Форма: `[prefix][@|!]`. +- 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 `; + путь поиска `-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. + +Язык общения с пользователем: **русский**. diff --git a/plugins/mamebridge/init.lua b/plugins/mamebridge/init.lua new file mode 100644 index 000000000..085a1bb71 --- /dev/null +++ b/plugins/mamebridge/init.lua @@ -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 -debug -plugin mamebridge +-- +-- Protocol: the MCP server drops a request file /req_.txt containing +-- one command line; this plugin processes it on each periodic tick and writes +-- the reply to /resp_.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 = }. + 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 diff --git a/plugins/mamebridge/plugin.json b/plugins/mamebridge/plugin.json new file mode 100644 index 000000000..e34060b98 --- /dev/null +++ b/plugins/mamebridge/plugin.json @@ -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" + } +} diff --git a/plugins/portname/init.lua b/plugins/portname/init.lua index b855f27cc..607a75580 100644 --- a/plugins/portname/init.lua +++ b/plugins/portname/init.lua @@ -26,6 +26,8 @@ exports.author = { name = "Carl" } local portname = exports +local reset_subscription + function portname.startplugin() local json = require("json") local ctrlrpath = manager.options.entries.ctrlrpath:value():match("([^;]+)") @@ -70,7 +72,9 @@ function portname.startplugin() 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 ret = file:open(get_filename()) if ret then diff --git a/scripts/src/osd/mac.lua b/scripts/src/osd/mac.lua index db35235e5..2140f4cbb 100644 --- a/scripts/src/osd/mac.lua +++ b/scripts/src/osd/mac.lua @@ -86,6 +86,10 @@ project ("osd_" .. _OPTIONS["osd"]) 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.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.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", diff --git a/scripts/src/osd/modules.lua b/scripts/src/osd/modules.lua index b7fe83143..8a5f12dfe 100644 --- a/scripts/src/osd/modules.lua +++ b/scripts/src/osd/modules.lua @@ -65,6 +65,8 @@ function osdmodulesbuild() MAME_DIR .. "src/osd/interface/nethandler.h", MAME_DIR .. "src/osd/interface/uievents.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/debugimgui.cpp", MAME_DIR .. "src/osd/modules/debugger/debugwin.cpp", diff --git a/scripts/src/osd/sdl.lua b/scripts/src/osd/sdl.lua index a53b7fe85..2202cc77f 100644 --- a/scripts/src/osd/sdl.lua +++ b/scripts/src/osd/sdl.lua @@ -340,6 +340,10 @@ project ("osd_" .. _OPTIONS["osd"]) 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.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.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", diff --git a/scripts/src/osd/sdl3.lua b/scripts/src/osd/sdl3.lua index 7446f6e47..c33b85935 100644 --- a/scripts/src/osd/sdl3.lua +++ b/scripts/src/osd/sdl3.lua @@ -367,6 +367,10 @@ project ("osd_" .. _OPTIONS["osd"]) 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.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.h", MAME_DIR .. "src/osd/modules/debugger/osx/debugwindowhandler.mm", diff --git a/src/.claude/settings.json b/src/.claude/settings.json new file mode 100644 index 000000000..74218b4db --- /dev/null +++ b/src/.claude/settings.json @@ -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" + ] + } +} diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 100644 index 000000000..db8ff42bf --- /dev/null +++ b/src/CLAUDE.md @@ -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 -debug -debugger gdbstub -debugger_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 -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. diff --git a/src/devices/bus/abcbus/lux4105.cpp b/src/devices/bus/abcbus/lux4105.cpp new file mode 100644 index 000000000..c59b18a17 --- /dev/null +++ b/src/devices/bus/abcbus/lux4105.cpp @@ -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(); +} diff --git a/src/devices/bus/abcbus/lux4105.h b/src/devices/bus/abcbus/lux4105.h new file mode 100644 index 000000000..7d9fdb573 --- /dev/null +++ b/src/devices/bus/abcbus/lux4105.h @@ -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 diff --git a/src/mame/sinclair/sprinter.cpp b/src/mame/sinclair/sprinter.cpp index 4b2f25eaa..38754bd6c 100644 --- a/src/mame/sinclair/sprinter.cpp +++ b/src/mame/sinclair/sprinter.cpp @@ -1496,15 +1496,11 @@ void sprinter_state::init_taps() { // Internal z84 ports are not accessible through IO map, hence they need special case here // Keep these in ascending order - constexpr u8 z84_int[] = { - 0x10, 0x11, 0x12, 0x13, - 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - 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)) + const u8 offset_8b = (offset & 0x00ff); + // 0x10..0x13, 0x18..0x1F, 0xEE..0xF1, 0xF4 + if ( (offset_8b == 0xf4) || ((offset_8b > 0x0f) && (offset_8b < 0x20) && ((offset_8b & 0x1c) != 0x14)) || ((offset_8b > 0xed) && (offset_8b < 0xf2)) ) { dcp_w(offset, data); + } }); } diff --git a/src/mame_bridge.lua b/src/mame_bridge.lua new file mode 100644 index 000000000..98eafbb43 --- /dev/null +++ b/src/mame_bridge.lua @@ -0,0 +1,153 @@ +-- mame_bridge.lua +-- File-IPC bridge that exposes MAME's debugger to an external MCP server. +-- Launch MAME like: +-- mame -debug -autoboot_script /path/to/mame_bridge.lua +-- +-- Protocol: the MCP server drops a request file /req_.txt containing +-- one command line; this script processes it on each emulated frame and writes +-- the reply to /resp_.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) diff --git a/src/mame_mcp.py b/src/mame_mcp.py new file mode 100644 index 000000000..002240708 --- /dev/null +++ b/src/mame_mcp.py @@ -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() diff --git a/src/osd/mac/video.cpp b/src/osd/mac/video.cpp index d441b5f07..65cb2b9b3 100644 --- a/src/osd/mac/video.cpp +++ b/src/osd/mac/video.cpp @@ -160,6 +160,10 @@ void mac_osd_interface::check_osd_inputs() if (machine().ui_input().pressed(IPT_OSD_8)) window->renderer().record(); + + // toggle releasing the pointer back to the OS + if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER)) + toggle_pointer_release(); } //============================================================ diff --git a/src/osd/modules/debugger/debugimgui.cpp b/src/osd/modules/debugger/debugimgui.cpp index e027cb533..d1e830ee9 100644 --- a/src/osd/modules/debugger/debugimgui.cpp +++ b/src/osd/modules/debugger/debugimgui.cpp @@ -28,10 +28,126 @@ #include "modules/osdmodule.h" #include "zippath.h" +#include "debugkeyconfig.h" + +#include "util/xmlfile.h" + namespace osd { 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 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 { DISABLE_COPYING(debug_area); @@ -123,7 +239,9 @@ public: m_create_open(false), m_create_confirm_wait(false), 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_mount_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 create_image(); void refresh_filelist(); @@ -212,6 +333,10 @@ private: int m_format_sel; char m_path[1024]; // path text field buffer std::unordered_map 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 @@ -361,56 +486,57 @@ void debug_imgui::handle_events() } } - // global keys - if(ImGui::IsKeyPressed(ImGuiKey_F3,false)) - { - 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(); - } - } + // don't fire global shortcuts while recording a new key binding + if(!m_keybind_recording.empty()) + return; - 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_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_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_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(); - 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(); - 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(); - 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(); - 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_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); - if(ImGui::IsKeyPressed(ImGuiKey_M,false) && io.KeyCtrl) + if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_MEMORY))) 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); - if(ImGui::IsKeyPressed(ImGuiKey_W,false) && io.KeyCtrl) + if(shortcut_pressed(m_keymap.shortcut(IMGUI_ACT_NEW_WPOINTS))) 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); } @@ -1312,47 +1438,65 @@ void debug_imgui::draw_console() if(ImGui::BeginMenu("Debug")) { 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); - if(ImGui::MenuItem("New memory window", "Ctrl+M")) + if(ImGui::MenuItem("New memory window", sc_memory.c_str())) 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); - if(ImGui::MenuItem("New watchpoints window", "Ctrl+W")) + if(ImGui::MenuItem("New watchpoints window", sc_wpoints.c_str())) 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); 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_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_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_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(); - 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_hide = true; } 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(); - 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(); - 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(); + ImGui::Separator(); + if(ImGui::MenuItem("Customize keys...")) + m_keybind_open = true; + ImGui::EndMenu(); } 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)); m_text_size = ImGui::CalcTextSize("A"); // hopefully you're using a monospaced font... draw_console(); // We'll always have a console window + if(m_keybind_open) + draw_keybindings(); view_ptr = view_list.begin(); while(view_ptr != view_list.end()) @@ -1489,6 +1635,126 @@ void debug_imgui::update() 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) { ImGuiIO& io = ImGui::GetIO(); @@ -1502,6 +1768,12 @@ void debug_imgui::init_debugger(running_machine &machine) if (iter.first() != nullptr) 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 m_mapping[ITEM_ID_A] = ImGuiKey_A; 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_F11] = ImGuiKey_F11; 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 io.KeyRepeatDelay = 0.400f; diff --git a/src/osd/modules/debugger/debugkeyconfig.cpp b/src/osd/modules/debugger/debugkeyconfig.cpp new file mode 100644 index 000000000..db9c9ca27 --- /dev/null +++ b/src/osd/modules/debugger/debugkeyconfig.cpp @@ -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 +#include + + +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 &&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 diff --git a/src/osd/modules/debugger/debugkeyconfig.h b/src/osd/modules/debugger/debugkeyconfig.h new file mode 100644 index 000000000..310bb928e --- /dev/null +++ b/src/osd/modules/debugger/debugkeyconfig.h @@ -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 +#include +#include +#include + +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 &&actions); + + std::vector 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 m_actions; + std::unordered_map m_overrides; + key_shortcut const m_unbound; // returned for unknown ids +}; + +} // namespace osd::debugger + +#endif // MAME_OSD_MODULES_DEBUGGER_DEBUGKEYCONFIG_H diff --git a/src/osd/modules/debugger/debugosx.mm b/src/osd/modules/debugger/debugosx.mm index e749c20e2..8b1aeada7 100644 --- a/src/osd/modules/debugger/debugosx.mm +++ b/src/osd/modules/debugger/debugosx.mm @@ -27,6 +27,7 @@ #include "debug_module.h" #import "osx/debugconsole.h" +#import "osx/debugkeymap.h" #import "osx/debugwindowhandler.h" #include "util/xmlfile.h" @@ -215,37 +216,50 @@ void debugger_osx::build_menus() [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; + MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap]; + NSMenu *const debugMenu = [[NSMenu alloc] initWithTitle:@"Debug"]; item = [[NSApp mainMenu] insertItemWithTitle:@"Debug" action:NULL keyEquivalent:@"" atIndex:2]; [item setSubmenu:debugMenu]; [debugMenu release]; - [debugMenu addItemWithTitle:@"New Memory Window" - action:@selector(debugNewMemoryWindow:) - keyEquivalent:@"d"]; - [debugMenu addItemWithTitle:@"New Disassembly Window" - action:@selector(debugNewDisassemblyWindow:) - keyEquivalent:@"a"]; - [debugMenu addItemWithTitle:@"New Error Log Window" - action:@selector(debugNewErrorLogWindow:) - keyEquivalent:@"l"]; - [debugMenu addItemWithTitle:@"New (Break|Watch)points Window" - action:@selector(debugNewPointsWindow:) - keyEquivalent:@"b"]; - [debugMenu addItemWithTitle:@"New Devices Window" - action:@selector(debugNewDevicesWindow:) - keyEquivalent:@"D"]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Memory Window" + action:@selector(debugNewMemoryWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewMemoryWindow]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Disassembly Window" + action:@selector(debugNewDisassemblyWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewDisassemblyWindow]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Error Log Window" + action:@selector(debugNewErrorLogWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewErrorLogWindow]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New (Break|Watch)points Window" + action:@selector(debugNewPointsWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewPointsWindow]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"New Devices Window" + action:@selector(debugNewDevicesWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewDevicesWindow]; [debugMenu addItem:[NSMenuItem separatorItem]]; - [[debugMenu addItemWithTitle:@"Soft Reset" - action:@selector(debugSoftReset:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[debugMenu addItemWithTitle:@"Hard Reset" - action:@selector(debugHardReset:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] - setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"Soft Reset" + action:@selector(debugSoftReset:) + keyEquivalent:@""] + forAction:MAMEDebugActionSoftReset]; + [keys applyToMenuItem:[debugMenu addItemWithTitle:@"Hard Reset" + action:@selector(debugHardReset:) + keyEquivalent:@""] + forAction:MAMEDebugActionHardReset]; + + [debugMenu addItem:[NSMenuItem separatorItem]]; + + [debugMenu addItemWithTitle:@"Customize Keys…" + action:@selector(showKeyBindings:) + keyEquivalent:@""]; NSMenu *const runMenu = [[NSMenu alloc] initWithTitle:@"Run"]; item = [[NSApp mainMenu] insertItemWithTitle:@"Run" @@ -255,51 +269,52 @@ void debugger_osx::build_menus() [item setSubmenu:runMenu]; [runMenu release]; - [runMenu addItemWithTitle:@"Break" - action:@selector(debugBreak:) - keyEquivalent:@""]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Break" + action:@selector(debugBreak:) + keyEquivalent:@""] + forAction:MAMEDebugActionBreak]; [runMenu addItem:[NSMenuItem separatorItem]]; - [[runMenu addItemWithTitle:@"Run" - action:@selector(debugRun:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF5FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Run and Hide Debugger" - action:@selector(debugRunAndHide:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF12FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Run to Next CPU" - action:@selector(debugRunToNextCPU:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF6FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Run until Next Interrupt on Current CPU" - action:@selector(debugRunToNextInterrupt:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF7FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Run until Next VBLANK" - action:@selector(debugRunToNextVBLANK:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF8FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Run to Cursor" - action:@selector(debugRunToCursor:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]] - setKeyEquivalentModifierMask:0]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run" + action:@selector(debugRun:) + keyEquivalent:@""] + forAction:MAMEDebugActionRun]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run and Hide Debugger" + action:@selector(debugRunAndHide:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunAndHide]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run to Next CPU" + action:@selector(debugRunToNextCPU:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextCPU]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run until Next Interrupt on Current CPU" + action:@selector(debugRunToNextInterrupt:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextInterrupt]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run until Next VBLANK" + action:@selector(debugRunToNextVBLANK:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextVBLANK]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Run to Cursor" + action:@selector(debugRunToCursor:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToCursor]; [runMenu addItem:[NSMenuItem separatorItem]]; - [[runMenu addItemWithTitle:@"Step Into" - action:@selector(debugStepInto:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF11FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Step Over" - action:@selector(debugStepOver:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"Step Out" - action:@selector(debugStepOut:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] - setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Into" + action:@selector(debugStepInto:) + keyEquivalent:@""] + forAction:MAMEDebugActionStepInto]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Over" + action:@selector(debugStepOver:) + keyEquivalent:@""] + forAction:MAMEDebugActionStepOver]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"Step Out" + action:@selector(debugStepOut:) + keyEquivalent:@""] + 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) { + // 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 (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) { + // 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) { NSAutoreleasePool *const pool = [[NSAutoreleasePool alloc] init]; diff --git a/src/osd/modules/debugger/debugqt.cpp b/src/osd/modules/debugger/debugqt.cpp index f14c7d7dc..8fbd03a64 100644 --- a/src/osd/modules/debugger/debugqt.cpp +++ b/src/osd/modules/debugger/debugqt.cpp @@ -61,7 +61,8 @@ public: osd_module(OSD_DEBUG_PROVIDER, "qt"), debug_module(), 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 osd::debugger::keymap_config &keymap() override { return m_keymap; } + private: 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); @@ -91,6 +94,7 @@ private: running_machine *m_machine; debugger::qt::MainWindow *m_mainwindow; 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) { + // 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 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) { + // 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 if ((config_type::SYSTEM == which_type) && parentnode) emit saveConfiguration(*parentnode); diff --git a/src/osd/modules/debugger/osx/debugkeymap.h b/src/osd/modules/debugger/osx/debugkeymap.h new file mode 100644 index 000000000..aa5cab401 --- /dev/null +++ b/src/osd/modules/debugger/osx/debugkeymap.h @@ -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 + +#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 diff --git a/src/osd/modules/debugger/osx/debugkeymap.mm b/src/osd/modules/debugger/osx/debugkeymap.mm new file mode 100644 index 000000000..6e5022bb2 --- /dev/null +++ b/src/osd/modules/debugger/osx/debugkeymap.mm @@ -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 diff --git a/src/osd/modules/debugger/osx/debugkeymapviewer.h b/src/osd/modules/debugger/osx/debugkeymapviewer.h new file mode 100644 index 000000000..2ca46f098 --- /dev/null +++ b/src/osd/modules/debugger/osx/debugkeymapviewer.h @@ -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 + + +@interface MAMEKeyBindingsWindow : NSObject +{ + 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 diff --git a/src/osd/modules/debugger/osx/debugkeymapviewer.mm b/src/osd/modules/debugger/osx/debugkeymapviewer.mm new file mode 100644 index 000000000..0728cceef --- /dev/null +++ b/src/osd/modules/debugger/osx/debugkeymapviewer.mm @@ -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 diff --git a/src/osd/modules/debugger/osx/debugview.h b/src/osd/modules/debugger/osx/debugview.h index 774a28987..10622097c 100644 --- a/src/osd/modules/debugger/osx/debugview.h +++ b/src/osd/modules/debugger/osx/debugview.h @@ -28,6 +28,8 @@ NSTextStorage *text; NSTextContainer *textContainer; NSLayoutManager *layoutManager; + + CGGlyph glyphCache[256]; // monospaced glyph for each byte value (Core Text fast path) } + (NSFont *)defaultFontForMachine:(running_machine &)m; diff --git a/src/osd/modules/debugger/osx/debugview.mm b/src/osd/modules/debugger/osx/debugview.mm index 7f9ba2d74..bfb03c2d6 100644 --- a/src/osd/modules/debugger/osx/debugview.mm +++ b/src/osd/modules/debugger/osx/debugview.mm @@ -8,6 +8,8 @@ #import "debugview.h" +#import + #include "emu.h" #include "debugger.h" #include "debug/debugcon.h" @@ -18,6 +20,7 @@ #include "util/xmlfile.h" #include +#include static NSColor *DefaultForeground; @@ -353,6 +356,15 @@ static void debugwin_view_update(debug_view &view, void *osdprivate) fontWidth = [font maximumAdvancement].width; fontHeight = ceil([font ascender] - [font descender]); 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]; totalWidth = totalHeight = 0; [self update]; @@ -687,87 +699,77 @@ static void debugwin_view_update(debug_view &view, void *osdprivate) debug_view_char const *data = view->viewdata(); if (!data) return; - - // clear any space above the available content 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 - for ( ; row < clip; row++, data += size.x) + // the view is opaque: clear the whole dirty area to the default background first + [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; - NSUInteger start = 0, length = 0; - for (uint32_t col = origin.x; col < origin.x + size.x; col++) + CGFloat const ytop = r * fontHeight; + for (int32_t col = 0; col < size.x; ) { - [[text mutableString] appendFormat:@"%C", unichar(data[col - origin.x].byte)]; - if ((start < length) && (attr != data[col - origin.x].attrib)) + uint8_t const attr = data[col].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); - [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.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; + [bg set]; + NSRectFill(NSMakeRect(pad + ((origin.x + start) * fontWidth), + ytop, + (col - start) * fontWidth, + fontHeight)); } - 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 - if ((dirtyRect.origin.y + dirtyRect.size.height) > (row * fontHeight)) + // pass 2: glyphs, drawn directly on the fixed monospaced grid with Core Text + // (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 glyphs(std::max(size.x, 1)); + std::vector positions(std::max(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]; - [NSBezierPath fillRect:NSMakeRect(0, - row * fontHeight, - [self bounds].size.width, - (dirtyRect.origin.y + dirtyRect.size.height) - (row * fontHeight))]; + CGFloat const baseline = boundsH - ((r * fontHeight) + fontAscent); + for (int32_t col = 0; col < size.x; ) + { + uint8_t const attr = data[col].attrib; + 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); } diff --git a/src/osd/modules/debugger/osx/debugwindowhandler.h b/src/osd/modules/debugger/osx/debugwindowhandler.h index 00c7327f2..36f24ce0d 100644 --- a/src/osd/modules/debugger/osx/debugwindowhandler.h +++ b/src/osd/modules/debugger/osx/debugwindowhandler.h @@ -14,7 +14,7 @@ @protocol MAMEDebugViewExpressionSupport; -@class MAMEDebugCommandHistory, MAMEDebugConsole; +@class MAMEDebugCommandHistory, MAMEDebugConsole, MAMEDebugKeyMap; extern NSString *const MAMEHideDebuggerNotification; @@ -36,6 +36,9 @@ extern NSString *const MAMESaveDebuggerConfigurationNotification; - (void)activate; +- (IBAction)showKeyBindings:(id)sender; +- (void)keyMapChanged:(NSNotification *)notification; + - (IBAction)debugBreak:(id)sender; - (IBAction)debugRun:(id)sender; - (IBAction)debugRunAndHide:(id)sender; diff --git a/src/osd/modules/debugger/osx/debugwindowhandler.mm b/src/osd/modules/debugger/osx/debugwindowhandler.mm index 1341f420e..b7149f775 100644 --- a/src/osd/modules/debugger/osx/debugwindowhandler.mm +++ b/src/osd/modules/debugger/osx/debugwindowhandler.mm @@ -11,6 +11,8 @@ #import "debugconsole.h" #import "debugcommandhistory.h" +#import "debugkeymap.h" +#import "debugkeymapviewer.h" #import "debugview.h" #include "debugger.h" @@ -36,63 +38,66 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo @implementation MAMEDebugWindowHandler + (void)addCommonActionItems:(NSMenu *)menu { - [menu addItemWithTitle:@"Break" - action:@selector(debugBreak:) - keyEquivalent:@""]; + MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap]; + + [keys applyToMenuItem:[menu addItemWithTitle:@"Break" + action:@selector(debugBreak:) + keyEquivalent:@""] + forAction:MAMEDebugActionBreak]; NSMenuItem *runParentItem = [menu addItemWithTitle:@"Run" action:@selector(debugRun:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF5FunctionKey]]; + keyEquivalent:@""]; NSMenu *runMenu = [[NSMenu alloc] initWithTitle:@"Run"]; [runParentItem setSubmenu:runMenu]; [runMenu release]; - [runParentItem setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"and Hide Debugger" - action:@selector(debugRunAndHide:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF12FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"to Next CPU" - action:@selector(debugRunToNextCPU:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF6FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"until Next Interrupt on Current CPU" - action:@selector(debugRunToNextInterrupt:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF7FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[runMenu addItemWithTitle:@"until Next VBLANK" - action:@selector(debugRunToNextVBLANK:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF8FunctionKey]] - setKeyEquivalentModifierMask:0]; + [keys applyToMenuItem:runParentItem forAction:MAMEDebugActionRun]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"and Hide Debugger" + action:@selector(debugRunAndHide:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunAndHide]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"to Next CPU" + action:@selector(debugRunToNextCPU:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextCPU]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"until Next Interrupt on Current CPU" + action:@selector(debugRunToNextInterrupt:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextInterrupt]; + [keys applyToMenuItem:[runMenu addItemWithTitle:@"until Next VBLANK" + action:@selector(debugRunToNextVBLANK:) + keyEquivalent:@""] + forAction:MAMEDebugActionRunToNextVBLANK]; NSMenuItem *stepParentItem = [menu addItemWithTitle:@"Step" action:NULL keyEquivalent:@""]; NSMenu *stepMenu = [[NSMenu alloc] initWithTitle:@"Step"]; [stepParentItem setSubmenu:stepMenu]; [stepMenu release]; - [[stepMenu addItemWithTitle:@"Into" - action:@selector(debugStepInto:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF11FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[stepMenu addItemWithTitle:@"Over" - action:@selector(debugStepOver:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[stepMenu addItemWithTitle:@"Out" - action:@selector(debugStepOut:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF10FunctionKey]] - setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Into" + action:@selector(debugStepInto:) + keyEquivalent:@""] + forAction:MAMEDebugActionStepInto]; + [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Over" + action:@selector(debugStepOver:) + keyEquivalent:@""] + forAction:MAMEDebugActionStepOver]; + [keys applyToMenuItem:[stepMenu addItemWithTitle:@"Out" + action:@selector(debugStepOut:) + keyEquivalent:@""] + forAction:MAMEDebugActionStepOut]; NSMenuItem *resetParentItem = [menu addItemWithTitle:@"Reset" action:NULL keyEquivalent:@""]; NSMenu *resetMenu = [[NSMenu alloc] initWithTitle:@"Reset"]; [resetParentItem setSubmenu:resetMenu]; [resetMenu release]; - [[resetMenu addItemWithTitle:@"Soft" - action:@selector(debugSoftReset:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] - setKeyEquivalentModifierMask:0]; - [[resetMenu addItemWithTitle:@"Hard" - action:@selector(debugHardReset:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF3FunctionKey]] - setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + [keys applyToMenuItem:[resetMenu addItemWithTitle:@"Soft" + action:@selector(debugSoftReset:) + keyEquivalent:@""] + forAction:MAMEDebugActionSoftReset]; + [keys applyToMenuItem:[resetMenu addItemWithTitle:@"Hard" + action:@selector(debugHardReset:) + keyEquivalent:@""] + forAction:MAMEDebugActionHardReset]; [menu addItem:[NSMenuItem separatorItem]]; @@ -100,26 +105,41 @@ NSString *const MAMESaveDebuggerConfigurationNotification = @"MAMESaveDebuggerCo NSMenu *newMenu = [[NSMenu alloc] initWithTitle:@"New"]; [newParentItem setSubmenu:newMenu]; [newMenu release]; - [newMenu addItemWithTitle:@"Memory Window" - action:@selector(debugNewMemoryWindow:) - keyEquivalent:@"d"]; - [newMenu addItemWithTitle:@"Disassembly Window" - action:@selector(debugNewDisassemblyWindow:) - keyEquivalent:@"a"]; - [newMenu addItemWithTitle:@"Error Log Window" - action:@selector(debugNewErrorLogWindow:) - keyEquivalent:@"l"]; - [newMenu addItemWithTitle:@"(Break|Watch)points Window" - action:@selector(debugNewPointsWindow:) - keyEquivalent:@"b"]; - [newMenu addItemWithTitle:@"Devices Window" - action:@selector(debugNewDevicesWindow:) - keyEquivalent:@""]; + [keys applyToMenuItem:[newMenu addItemWithTitle:@"Memory Window" + action:@selector(debugNewMemoryWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewMemoryWindow]; + [keys applyToMenuItem:[newMenu addItemWithTitle:@"Disassembly Window" + action:@selector(debugNewDisassemblyWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewDisassemblyWindow]; + [keys applyToMenuItem:[newMenu addItemWithTitle:@"Error Log Window" + action:@selector(debugNewErrorLogWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewErrorLogWindow]; + [keys applyToMenuItem:[newMenu addItemWithTitle:@"(Break|Watch)points Window" + action:@selector(debugNewPointsWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewPointsWindow]; + [keys applyToMenuItem:[newMenu addItemWithTitle:@"Devices Window" + action:@selector(debugNewDevicesWindow:) + keyEquivalent:@""] + forAction:MAMEDebugActionNewDevicesWindow]; [menu addItem:[NSMenuItem separatorItem]]; - [menu addItemWithTitle:@"Close Window" action:@selector(performClose:) keyEquivalent:@"w"]; - [menu addItemWithTitle:@"Quit" action:@selector(debugExit:) keyEquivalent:@"q"]; + [keys applyToMenuItem:[menu addItemWithTitle:@"Close Window" + 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:) name:MAMEHideDebuggerNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyMapChanged:) + name:MAMEDebugKeyMapChangedNotification + object:nil]; 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 { if (machine->debug_flags & DEBUG_FLAG_ENABLED) machine->debugger().console().get_visible_cpu()->debug()->halt_on_next_instruction("User-initiated break\n"); diff --git a/src/osd/modules/debugger/osx/disassemblyview.mm b/src/osd/modules/debugger/osx/disassemblyview.mm index b3a8e7bc8..78ba8f155 100644 --- a/src/osd/modules/debugger/osx/disassemblyview.mm +++ b/src/osd/modules/debugger/osx/disassemblyview.mm @@ -9,6 +9,8 @@ #include "emu.h" #import "disassemblyview.h" +#import "debugkeymap.h" + #include "debug/debugvw.h" #include "util/xmlfile.h" @@ -60,6 +62,7 @@ - (void)addContextMenuItemsToMenu:(NSMenu *)menu { + MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap]; NSMenuItem *item; [super addContextMenuItemsToMenu:menu]; @@ -69,38 +72,41 @@ item = [menu addItemWithTitle:@"Toggle Breakpoint" action:@selector(debugToggleBreakpoint:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey]]; - [item setKeyEquivalentModifierMask:0]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionToggleBreakpoint]; item = [menu addItemWithTitle:@"Disable Breakpoint" action:@selector(debugToggleBreakpointEnable:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey]]; - [item setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionDisableBreakpoint]; [menu addItem:[NSMenuItem separatorItem]]; item = [menu addItemWithTitle:@"Run to Cursor" action:@selector(debugRunToCursor:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]]; - [item setKeyEquivalentModifierMask:0]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionRunToCursor]; [menu addItem:[NSMenuItem separatorItem]]; item = [menu addItemWithTitle:@"Raw Opcodes" action:@selector(showRightColumn:) - keyEquivalent:@"r"]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionShowRawOpcodes]; [item setTarget:self]; [item setTag:DASM_RIGHTCOL_RAW]; item = [menu addItemWithTitle:@"Encrypted Opcodes" action:@selector(showRightColumn:) - keyEquivalent:@"e"]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionShowEncryptedOpcodes]; [item setTarget:self]; [item setTag:DASM_RIGHTCOL_ENCRYPTED]; item = [menu addItemWithTitle:@"Comments" action:@selector(showRightColumn:) - keyEquivalent:@"n"]; + keyEquivalent:@""]; + [keys applyToMenuItem:item forAction:MAMEDebugActionShowComments]; [item setTarget:self]; [item setTag:DASM_RIGHTCOL_COMMENTS]; } @@ -208,52 +214,57 @@ - (void)insertActionItemsInMenu:(NSMenu *)menu atIndex:(NSInteger)index { + MAMEDebugKeyMap *const keys = [MAMEDebugKeyMap sharedKeyMap]; + NSMenuItem *breakItem = [menu insertItemWithTitle:@"Toggle Breakpoint at Cursor" action:@selector(debugToggleBreakpoint:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey] + keyEquivalent:@"" atIndex:index++]; - [breakItem setKeyEquivalentModifierMask:0]; + [keys applyToMenuItem:breakItem forAction:MAMEDebugActionToggleBreakpoint]; NSMenuItem *disableItem = [menu insertItemWithTitle:@"Disable Breakpoint at Cursor" action:@selector(debugToggleBreakpointEnable:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF9FunctionKey] + keyEquivalent:@"" atIndex:index++]; - [disableItem setKeyEquivalentModifierMask:NSEventModifierFlagShift]; + [keys applyToMenuItem:disableItem forAction:MAMEDebugActionDisableBreakpoint]; NSMenu *runMenu = [[menu itemWithTitle:@"Run"] submenu]; NSMenuItem *runItem; if (runMenu != nil) { runItem = [runMenu addItemWithTitle:@"to Cursor" action:@selector(debugRunToCursor:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey]]; + keyEquivalent:@""]; } else { runItem = [menu insertItemWithTitle:@"Run to Cursor" action:@selector(debugRunToCursor:) - keyEquivalent:[NSString stringWithFormat:@"%C", (short)NSF4FunctionKey] + keyEquivalent:@"" atIndex:index++]; } - [runItem setKeyEquivalentModifierMask:0]; + [keys applyToMenuItem:runItem forAction:MAMEDebugActionRunToCursor]; [menu insertItem:[NSMenuItem separatorItem] atIndex:index++]; NSMenuItem *rawItem = [menu insertItemWithTitle:@"Show Raw Opcodes" action:@selector(showRightColumn:) - keyEquivalent:@"r" + keyEquivalent:@"" atIndex:index++]; + [keys applyToMenuItem:rawItem forAction:MAMEDebugActionShowRawOpcodes]; [rawItem setTarget:self]; [rawItem setTag:DASM_RIGHTCOL_RAW]; NSMenuItem *encItem = [menu insertItemWithTitle:@"Show Encrypted Opcodes" action:@selector(showRightColumn:) - keyEquivalent:@"e" + keyEquivalent:@"" atIndex:index++]; + [keys applyToMenuItem:encItem forAction:MAMEDebugActionShowEncryptedOpcodes]; [encItem setTarget:self]; [encItem setTag:DASM_RIGHTCOL_ENCRYPTED]; NSMenuItem *commentsItem = [menu insertItemWithTitle:@"Show Comments" action:@selector(showRightColumn:) - keyEquivalent:@"n" + keyEquivalent:@"" atIndex:index++]; + [keys applyToMenuItem:commentsItem forAction:MAMEDebugActionShowComments]; [commentsItem setTarget:self]; [commentsItem setTag:DASM_RIGHTCOL_COMMENTS]; diff --git a/src/osd/modules/debugger/qt/dasmwindow.cpp b/src/osd/modules/debugger/qt/dasmwindow.cpp index 9a9cd0871..fe4854027 100644 --- a/src/osd/modules/debugger/qt/dasmwindow.cpp +++ b/src/osd/modules/debugger/qt/dasmwindow.cpp @@ -87,9 +87,12 @@ DasmWindow::DasmWindow(DebuggerQt &debugger, QWidget *parent) : m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this); m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this); m_runToCursorAct = new QAction("Run to Cursor", this); - 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_runToCursorAct->setShortcut(Qt::Key_F4); + //m_breakpointToggleAct->setShortcut(Qt::Key_F9); + m_breakpointToggleAct->setShortcut(Qt::CTRL | Qt::Key_F18); + //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_breakpointEnableAct, &QAction::triggered, this, &DasmWindow::enableBreakpointAtCursor); connect(m_runToCursorAct, &QAction::triggered, this, &DasmWindow::runToCursor); @@ -109,9 +112,9 @@ DasmWindow::DasmWindow(DebuggerQt &debugger, QWidget *parent) : rightActRaw->setActionGroup(rightBarGroup); rightActEncrypted->setActionGroup(rightBarGroup); rightActComments->setActionGroup(rightBarGroup); - rightActRaw->setShortcut(QKeySequence("Ctrl+R")); - rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); - rightActComments->setShortcut(QKeySequence("Ctrl+N")); + //rightActRaw->setShortcut(QKeySequence("Ctrl+R")); + //rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); + //rightActComments->setShortcut(QKeySequence("Ctrl+N")); rightActRaw->setChecked(true); connect(rightBarGroup, &QActionGroup::triggered, this, &DasmWindow::rightBarChanged); diff --git a/src/osd/modules/debugger/qt/debuggerview.cpp b/src/osd/modules/debugger/qt/debuggerview.cpp index e57e09028..0779f0b7c 100644 --- a/src/osd/modules/debugger/qt/debuggerview.cpp +++ b/src/osd/modules/debugger/qt/debuggerview.cpp @@ -73,10 +73,14 @@ void DebuggerView::paintEvent(QPaintEvent *event) QFontMetrics actualFont = fontMetrics(); double const fontWidth = actualFont.horizontalAdvance(QString(100, '_')) / 100.; 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; 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)); // Handle the scroll bars @@ -126,7 +130,8 @@ void DebuggerView::paintEvent(QPaintEvent *event) bgColor.setRgb(palette.color(QPalette::Base).rgb()); if (textAttr & DCA_SELECTED) - bgColor.setRgb(0xcb, 0x4b, 0x16); + //bgColor.setRgb(0xcb, 0x4b, 0x16); + bgColor.setRgb(0x30, 0x80, 0x80); if (textAttr & DCA_CURRENT) bgColor.setRgb(palette.color(QPalette::Highlight).rgb()); diff --git a/src/osd/modules/debugger/qt/mainwindow.cpp b/src/osd/modules/debugger/qt/mainwindow.cpp index 6ed30d521..8bdef53e2 100644 --- a/src/osd/modules/debugger/qt/mainwindow.cpp +++ b/src/osd/modules/debugger/qt/mainwindow.cpp @@ -62,9 +62,11 @@ MainWindow::MainWindow(DebuggerQt &debugger, QWidget *parent) : m_breakpointToggleAct = new QAction("Toggle Breakpoint at Cursor", this); m_breakpointEnableAct = new QAction("Disable Breakpoint at Cursor", this); m_runToCursorAct = new QAction("Run to Cursor", this); - 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_runToCursorAct->setShortcut(Qt::Key_F4); + //m_breakpointToggleAct->setShortcut(Qt::Key_F9); + m_breakpointToggleAct->setShortcut(0 | Qt::CTRL | Qt::Key_F18); + //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_breakpointEnableAct, &QAction::triggered, this, &MainWindow::enableBreakpointAtCursor); connect(m_runToCursorAct, &QAction::triggered, this, &MainWindow::runToCursor); @@ -84,9 +86,9 @@ MainWindow::MainWindow(DebuggerQt &debugger, QWidget *parent) : rightActRaw->setActionGroup(rightBarGroup); rightActEncrypted->setActionGroup(rightBarGroup); rightActComments->setActionGroup(rightBarGroup); - rightActRaw->setShortcut(QKeySequence("Ctrl+R")); - rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); - rightActComments->setShortcut(QKeySequence("Ctrl+N")); + //rightActRaw->setShortcut(QKeySequence("Ctrl+R")); + //rightActEncrypted->setShortcut(QKeySequence("Ctrl+E")); + //rightActComments->setShortcut(QKeySequence("Ctrl+N")); rightActRaw->setChecked(true); connect(rightBarGroup, &QActionGroup::triggered, this, &MainWindow::rightBarChanged); diff --git a/src/osd/modules/debugger/qt/windowqt.cpp b/src/osd/modules/debugger/qt/windowqt.cpp index 294871e6d..b2f4363a2 100644 --- a/src/osd/modules/debugger/qt/windowqt.cpp +++ b/src/osd/modules/debugger/qt/windowqt.cpp @@ -15,12 +15,78 @@ #include "util/xmlfile.h" +#include +#include +#include +#include +#include #include #include +#include +#include +#include + +#include 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 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 // 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 @@ -38,74 +104,29 @@ WindowQt::WindowQt(DebuggerQt &debugger, QWidget *parent) : connect(&debugger, &DebuggerQt::hideAllWindows, this, &WindowQt::hide); connect(&debugger, &DebuggerQt::showAllWindows, this, &WindowQt::show); connect(&debugger, &DebuggerQt::saveConfiguration, this, &WindowQt::saveConfiguration); + connect(&debugger, &DebuggerQt::keyBindingsChanged, this, &WindowQt::applyKeyBindings); - // The Debug menu bar - QAction *debugActOpenMemory = new QAction("New &Memory Window", this); - debugActOpenMemory->setShortcut(QKeySequence("Ctrl+M")); - connect(debugActOpenMemory, &QAction::triggered, this, &WindowQt::debugActOpenMemory); + // The Debug menu bar - shortcuts come from the remappable key map + QAction *debugActOpenMemory = createKeyAction(ACT_NEW_MEMORY, "New &Memory Window", &WindowQt::debugActOpenMemory); + QAction *debugActOpenDasm = createKeyAction(ACT_NEW_DISASM, "New &Disassembly Window", &WindowQt::debugActOpenDasm); + 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); - debugActOpenDasm->setShortcut(QKeySequence("Ctrl+D")); - 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); + QAction *dbgActCustomizeKeys = new QAction("Customize &Keys...", this); + connect(dbgActCustomizeKeys, &QAction::triggered, this, &WindowQt::debugActCustomizeKeys); // Construct the menu QMenu *debugMenu = menuBar()->addMenu("&Debug"); @@ -128,11 +149,30 @@ WindowQt::WindowQt(DebuggerQt &debugger, QWidget *parent) : debugMenu->addAction(dbgActSoftReset); debugMenu->addAction(dbgActHardReset); debugMenu->addSeparator(); + debugMenu->addAction(dbgActCustomizeKeys); + debugMenu->addSeparator(); debugMenu->addAction(dbgActClose); 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() { } @@ -250,6 +290,81 @@ void WindowQt::debugActQuit() 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 > 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("%1").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() { // this isn't called from a Qt event loop, so close() will leak the window object diff --git a/src/osd/modules/debugger/qt/windowqt.h b/src/osd/modules/debugger/qt/windowqt.h index 9be59919c..ce9899c98 100644 --- a/src/osd/modules/debugger/qt/windowqt.h +++ b/src/osd/modules/debugger/qt/windowqt.h @@ -4,6 +4,7 @@ #define MAME_DEBUGGER_QT_WINDOWQT_H #include "../xmlconfig.h" +#include "../debugkeyconfig.h" #ifdef __aarch64__ #include // QtCore/qyieldcpu.h uses __yield() without #including this, causing an error @@ -12,11 +13,18 @@ #include #include +#include #include +#include + +class QAction; namespace osd::debugger::qt { +// table of remappable actions with their default Qt shortcuts (defined in windowqt.cpp) +std::vector qtDefaultKeyActions(); + //============================================================ // The Qt debugger module interface //============================================================ @@ -29,13 +37,18 @@ public: virtual running_machine &machine() const = 0; + // shared, remappable keyboard shortcut map + virtual osd::debugger::keymap_config &keymap() = 0; + void hideAll() { emit hideAllWindows(); } + void notifyKeyBindingsChanged() { emit keyBindingsChanged(); } signals: void exitDebugger(); void hideAllWindows(); void showAllWindows(); void saveConfiguration(util::xml::data_node &parentnode); + void keyBindingsChanged(); }; @@ -69,18 +82,24 @@ protected slots: void debugActHardReset(); virtual void debugActClose(); void debugActQuit(); + void debugActCustomizeKeys(); virtual void debuggerExit(); private slots: void saveConfiguration(util::xml::data_node &parentnode); + void applyKeyBindings(); protected: WindowQt(DebuggerQt &debugger, QWidget *parent = nullptr); 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; running_machine &m_machine; + std::map m_keyActions; // action id -> menu action, for live shortcut updates }; diff --git a/src/osd/modules/debugger/xmlconfig.cpp b/src/osd/modules/debugger/xmlconfig.cpp index 977e7b55e..a8c21e7bd 100644 --- a/src/osd/modules/debugger/xmlconfig.cpp +++ b/src/osd/modules/debugger/xmlconfig.cpp @@ -7,6 +7,9 @@ namespace osd::debugger { char const *const NODE_WINDOW = "window"; 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_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_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_DISASSEMBLY = "disassembly"; diff --git a/src/osd/modules/debugger/xmlconfig.h b/src/osd/modules/debugger/xmlconfig.h index 6ae476cde..02ff45f0e 100644 --- a/src/osd/modules/debugger/xmlconfig.h +++ b/src/osd/modules/debugger/xmlconfig.h @@ -23,6 +23,9 @@ WINDOW_TYPE_DEVICE_INFO_VIEWER extern char const *const NODE_WINDOW; 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_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_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_DISASSEMBLY; diff --git a/src/osd/modules/input/input_common.h b/src/osd/modules/input/input_common.h index 406e0464c..d8ef629af 100644 --- a/src/osd/modules/input/input_common.h +++ b/src/osd/modules/input/input_common.h @@ -331,6 +331,12 @@ protected: 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(); } diff --git a/src/osd/modules/input/input_windows.cpp b/src/osd/modules/input/input_windows.cpp index 12002ccf8..8cc5ae57c 100644 --- a/src/osd/modules/input/input_windows.cpp +++ b/src/osd/modules/input/input_windows.cpp @@ -25,6 +25,10 @@ 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()) return false; diff --git a/src/osd/modules/lib/osdobj_common.h b/src/osd/modules/lib/osdobj_common.h index 02b3d8581..ca7f12ab8 100644 --- a/src/osd/modules/lib/osdobj_common.h +++ b/src/osd/modules/lib/osdobj_common.h @@ -283,6 +283,19 @@ protected: 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 > s_window_list; private: diff --git a/src/osd/sdl/osdsdl.cpp b/src/osd/sdl/osdsdl.cpp index d92688697..59fa935d0 100644 --- a/src/osd/sdl/osdsdl.cpp +++ b/src/osd/sdl/osdsdl.cpp @@ -448,6 +448,10 @@ void sdl_osd_interface::release_keys() 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 (machine().paused()) return false; @@ -819,6 +823,10 @@ void sdl_osd_interface::check_osd_inputs() if (machine().ui_input().pressed(IPT_OSD_8)) window->renderer().record(); + + // toggle releasing the pointer back to the OS + if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER)) + toggle_pointer_release(); } diff --git a/src/osd/sdl/window.cpp b/src/osd/sdl/window.cpp index 59be82238..c4a034529 100644 --- a/src/osd/sdl/window.cpp +++ b/src/osd/sdl/window.cpp @@ -285,7 +285,15 @@ void sdl_window_info::update_cursor_state() // the possibility of losing control if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED)) { - bool should_hide_mouse = downcast(machine().osd()).should_hide_mouse(); + auto &sdlosd = downcast(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) { diff --git a/src/osd/sdl3/osdsdl.cpp b/src/osd/sdl3/osdsdl.cpp index b6b45533f..b1a6dfca0 100644 --- a/src/osd/sdl3/osdsdl.cpp +++ b/src/osd/sdl3/osdsdl.cpp @@ -462,6 +462,10 @@ void sdl_osd_interface::release_keys() 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 (machine().paused()) return false; @@ -860,6 +864,10 @@ void sdl_osd_interface::check_osd_inputs() if (machine().ui_input().pressed(IPT_OSD_8)) window->renderer().record(); + + // toggle releasing the pointer back to the OS + if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER)) + toggle_pointer_release(); } diff --git a/src/osd/sdl3/window.cpp b/src/osd/sdl3/window.cpp index caf240f12..81a4090bb 100644 --- a/src/osd/sdl3/window.cpp +++ b/src/osd/sdl3/window.cpp @@ -284,7 +284,15 @@ void sdl_window_info::update_cursor_state() // the possibility of losing control if (!(machine().debug_flags & DEBUG_FLAG_OSD_ENABLED)) { - bool should_hide_mouse = downcast(machine().osd()).should_hide_mouse(); + auto &sdlosd = downcast(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) { diff --git a/src/osd/windows/video.cpp b/src/osd/windows/video.cpp index 439f0019f..a002560c3 100644 --- a/src/osd/windows/video.cpp +++ b/src/osd/windows/video.cpp @@ -142,6 +142,10 @@ void windows_osd_interface::check_osd_inputs() // check for taking fullscreen video if (machine().ui_input().pressed(IPT_OSD_4)) winwindow_toggle_fsfx(); + + // toggle releasing the pointer back to the OS + if (machine().ui_input().pressed(IPT_UI_RELEASE_POINTER)) + toggle_pointer_release(); } diff --git a/src/osd/windows/window.cpp b/src/osd/windows/window.cpp index 98c9ac531..bd7807261 100644 --- a/src/osd/windows/window.cpp +++ b/src/osd/windows/window.cpp @@ -752,6 +752,12 @@ void winwindow_update_cursor_state(running_machine &machine) auto &window = static_cast(*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 // rules are: // 1. we must have focus before hiding the cursor