3d586af031
- 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>
134 lines
9.6 KiB
Markdown
134 lines
9.6 KiB
Markdown
# Клавиатура в играх на 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
|
||
|
||
```c
|
||
#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.
|