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

134 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Клавиатура в играх на 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.