Files
Sprinter-SDCC/docs/kbd-games.md
T
snark13 3d586af031 libc/kbd: raw-клавиатура — вычерпывание FIFO + селективный wipe модификаторов
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
  фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
  клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
  скан-кодов в docs/libc-reference.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:12 +03:00

9.6 KiB
Raw Permalink Blame History

Клавиатура в играх на Sprinter (raw-канал, held-state)

Как правильно читать клавиатуру в play-цикле: что даёт платформа, чего она НЕ даёт, и какие паттерны использовать. Выжато из порта Prince of Persia (applications/PoP/roomtest) и отладки багов «залипание клавиш» (2026-07-16) и «отвал Shift» (2026-07-22).

Почему не ESTEX

ESTEX-функции (kbhit/getch/getkey, WAITKEY/SCANKEY) — событийные: нажатие кладётся в буфер, отпускание не видно вообще. Live-состояние есть только у модификаторов (CTRLKEY/kbd_mod_state) — и то с багом BIOS: для стрелок/Home/End/PgUp/PgDn признак Shift не выставляется (docs/converted/bugs.txt §4). Игре же нужно «клавиша X зажата ПРЯМО СЕЙЧАС» для каждой клавиши. Отсюда raw-канал <kbd_raw.h>.

Матчасть: как клавиатура устроена снизу

  • AT/PS-2 клавиатура сидит на SIO-A Z84C015: порт 0x18 — данные (деструктивное чтение!), 0x19 — статус (RR0, бит 0 = «байт принят»).
  • Прерывание клавиатуры приходит с тем же IM2-вектором 0xFF, что и кадровое; различаются битом 0 порта 0x19.
  • Приёмный FIFO — 3 байта. Обработчик ОБЯЗАН вычерпывать его в цикле «пока бит 0 установлен» (docs/converted/IvanMak.txt §9.4), иначе пачка байт переполнит FIFO и байты потеряются.
  • Поток — PS/2 Scan Code Set 2: код = нажатие (make), F0 код = отпускание (break), E0 — префикс расширенных клавиш (стрелки и т.п.), т.е. отпускание стрелки = E0 F0 код. Быстрый тап стрелки = 5 байт подряд.
  • Typematic (автоповтор): повторяется только ПОСЛЕДНЯЯ нажатая клавиша — make-код шлётся снова и снова без break между ними. Модификаторы, зажатые вместе с другой клавишей, не шлют НИЧЕГО. Это ключевой факт для дизайна recovery (см. ниже).

API libc

#include <kbd_raw.h>

kbd_raw_open();               // забрать клавиатуру у DSS (весь поток наш)
...
while (!kbd_raw_down(KBD_ESC)) {   // выход проверяем САМИ — DSS слеп
    kbd_raw_sync();                // раз в кадр, ДО чтения клавиш
    if (kbd_raw_down(KBD_RIGHT)) ...
    if (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) ...
}
kbd_raw_close();              // вернуть клавиатуру DSS (есть и на atexit)
  • Пока канал открыт, DSS клавиатуру не видит: kbhit/getch/getkey/ kbd_mod_state заморожены. Перед экраном с консольным вводом — kbd_raw_close().
  • Декодер живёт прямо в IM2-трамплине (libc/irq/_irq_tramp.c): drain- цикл FIFO + FSM префиксов F0/E0 + битмап _kbdraw_down[512] (0..255 обычные, 256..511 расширенные, KBD_EXT).
  • kbd_raw_down(code) — O(1) чтение битмапа, зовите сколько угодно.
  • Требование памяти: BSS модуля должен быть в W2 (в --memory small у крошечных программ может уехать в W1 → kbd_raw_open вернёт EINVAL; huge/big — всегда ок).

Rx-overrun и политика восстановления

Если прерывания запрещены дольше ~3 байт-тактов (длинные DI-окна, тяжёлый кадр), FIFO переполняется, SIO теряет байты и взводит Rx Overrun (RR1 бит 5). Потерянный break = залипшая клавиша. Трамплин ловит это и взводит флаг; kbd_raw_sync() раз в кадр делает восстановление:

  • сбрасываются ВСЕ обычные клавиши — реально зажатые перечитаются ближайшим typematic-повтором (~0.1 с), а залипшие исчезнут;
  • модификаторы (Shift/Ctrl/Alt) НЕ сбрасываются — их перечитать нечем (typematic по ним не идёт), сброс превращался в «отвал» Shift при каждом overrun (баг PoP: Shift+→ давал 1-4 осторожных шага, после чего Кид начинал бежать — Shift пропадал из битмапа навсегда).

Цена компромисса (осознанная):

  • залипший модификатор (если overrun потерял именно его break) живёт до следующего нажатия этого модификатора — редкий случай;
  • дырка с аккордами: держим →, тапаем ↑ (run-jump) — теперь typematic идёт по ↑, и если overrun случится ДО отпускания →, стрелка сбросится и не перечитается (повтор к предыдущей клавише не возвращается). По замерам в MAME overrun'ы при стабильном удержании не возникают вовсе (они кластеризуются в момент пачек make/break при тапах), поэтому на практике окно узкое.

Паттерны игрового цикла

  • held-state против «свежего нажатия»: битмап отвечает только на «зажата ли». Для действий «одно нажатие = одно срабатывание» нужен edge-detect: if (sp && !sp_prev) toggle(); sp_prev = sp; (roomtest.c, тумблер дабл-буфера по SPACE).
  • Подавление автоповтора действий — конечный автомат RELEASED/HELD/IGNORE в стиле SDLPoP (pop_ctrl.c, read_user_control): действие срабатывает на переходе RELEASED→HELD, затем переводится в IGNORE и не повторяется, пока клавишу физически не отпустят. Битмап при этом остаётся level-triggered.
  • Оси: собирать control_x/control_y из пар клавиш каждый кадр из битмапа заново, не копить дельты.
  • kbd_raw_sync() звать строго один раз в кадр и строго ДО опроса клавиш этого кадра.

Тестирование в MAME (bridge)

  • Держать клавиши через set_input на :kbd:ms_naturl:* защёлкой: value=1 без frames, потом явный value=0. Вариант с frames=N для удержаний ненадёжен (холд может не породить ни одного скан-кода). Комбинации вида Shift+стрелка так подаются нормально (LShift = :kbd:ms_naturl:P1.7 mask 0x0002, Cursor Right = :kbd:ms_naturl:P2.4 mask 0x0040) — проверено 2026-07-22.
  • Наблюдать состояние — по символам map-файла: _kbdraw_down (+0x12 = LShift, +0x174 = Right и т.д.), _kbdraw_overrun; счётчики событий — watchpoint с действием {tempN=tempN+1; g} (не останавливает эмуляцию).
  • Не тестировать вдвоём одновременно (человек за клавиатурой + бридж-эмуляция): незакрытая защёлка set_input выглядит как «залипшая» клавиша и съедает часы отладки.
  • Overrun'ы в MAME заметно чаще, чем ожидается на железе (пачка тапа прилетает плотнее реальных ~1 мс/байт); открытый вопрос — эмулирует ли MAME прерывание на каждый принятый байт SIO (см. TODO).

История багов (чтобы не повторять)

  1. Залипание клавиш (2026-07-16): трамплин читал 1 байт за прерывание → FIFO(3) переполнялся пачкой break-кодов → break терялся → клавиша зажата навсегда. Фикс: drain-цикл в ISR (как эталоны docs/samples/sprinterKeybLib.asm, SIO_CTC_KEY.asm).
  2. Отвал Shift (2026-07-22): recovery по overrun сбрасывал ВЕСЬ битмап; модификаторы не перечитываются typematic'ом → Shift «отпускался» до перенажатия. Фикс: селективный сброс (модификаторы сохраняются), см. libc/kbd/kbd_raw_sync.c.

Связанные документы: docs/mame-autotest.md, docs/im2_isr_design.md, docs/converted/IvanMak.txt §9.4, applications/PoP/docs/PORT_PLAN.md §2.