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>
This commit is contained in:
@@ -276,6 +276,18 @@ Quick wins:
|
|||||||
CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков,
|
CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков,
|
||||||
которые должны были обслужить CBL-насос, уходят мимо, кольцо
|
которые должны были обслужить CBL-насос, уходят мимо, кольцо
|
||||||
недоливается. Нужно исследование (MAME-дамп/лог трамплина).
|
недоливается. Нужно исследование (MAME-дамп/лог трамплина).
|
||||||
|
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
|
||||||
|
2026-07-22) — замеры watchpoint-счётчиками: при стабильном
|
||||||
|
удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
|
||||||
|
E0 74 + E0 F0 74 при FIFO 3) даёт overrun. Подозрение: dev-MAME
|
||||||
|
не эмулирует прерывание SIO на каждый принятый байт (байты
|
||||||
|
вычерпываются только кадровым IRQ 50 Гц) и/или подаёт пачку без
|
||||||
|
реальных ~1 мс/байт — на железе per-byte INT должен делать
|
||||||
|
overrun'ы редкостью. Проверить mame/sources/MAME/src/mame/
|
||||||
|
sinclair/sprinter.cpp (путь байта клавиатуры → INT) и при
|
||||||
|
желании поправить dev-MAME. Recovery-политика уже терпима к
|
||||||
|
частым overrun'ам (селективный wipe, docs/kbd-games.md), но
|
||||||
|
аккорд «держу →, тапнул ↑» остаётся уязвим.
|
||||||
|
|
||||||
## Known quirks (зафиксированы, обходы в libc)
|
## Known quirks (зафиксированы, обходы в libc)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Клавиатура в играх на 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.
|
||||||
@@ -312,8 +312,10 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс
|
|||||||
| `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) |
|
| `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) |
|
||||||
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
|
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
|
||||||
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
|
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
|
||||||
|
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
|
||||||
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
|
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
|
||||||
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
|
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
|
||||||
|
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |
|
||||||
|
|
||||||
**ИСПРАВЛЕНО 2026-07-15 (был неверный вывод, ниже — то, что реально
|
**ИСПРАВЛЕНО 2026-07-15 (был неверный вывод, ниже — то, что реально
|
||||||
подтвердилось):** изначально скриншот-тестирование (снимок экрана
|
подтвердилось):** изначально скриншот-тестирование (снимок экрана
|
||||||
|
|||||||
+11
-3
@@ -48,9 +48,13 @@ uint8_t kbd_raw_down(uint16_t code);
|
|||||||
|
|
||||||
/* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения
|
/* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения
|
||||||
* kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка
|
* kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка
|
||||||
* скан-кодов (напр. shift+стрелка release) переполняет его → потерян break
|
* скан-кодов (напр. быстрый тап стрелки: make+break = 5 байт) переполняет
|
||||||
* → залипшая клавиша. Трамплин ловит overrun и взводит флаг; kbd_raw_sync
|
* его → потерян break → залипшая клавиша. Трамплин ловит overrun и
|
||||||
* по флагу сбрасывает ВСЁ held-состояние (реально зажатые перечитаются). */
|
* взводит флаг; kbd_raw_sync по флагу сбрасывает held-состояние всех
|
||||||
|
* клавиш КРОМЕ модификаторов (Shift/Ctrl/Alt): PS/2 автоповторяет только
|
||||||
|
* последнюю нажатую клавишу, поэтому обычные зажатые перечитаются
|
||||||
|
* typematic'ом, а сброшенный модификатор восстановить нечем — он бы
|
||||||
|
* «отваливался» при каждом overrun (см. kbd_raw_sync.c). */
|
||||||
void kbd_raw_sync(void);
|
void kbd_raw_sync(void);
|
||||||
|
|
||||||
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
|
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
|
||||||
@@ -66,5 +70,9 @@ void kbd_raw_sync(void);
|
|||||||
#define KBD_ESC 0x76
|
#define KBD_ESC 0x76
|
||||||
#define KBD_LSHIFT 0x12
|
#define KBD_LSHIFT 0x12
|
||||||
#define KBD_RSHIFT 0x59
|
#define KBD_RSHIFT 0x59
|
||||||
|
#define KBD_LCTRL 0x14
|
||||||
|
#define KBD_LALT 0x11
|
||||||
|
#define KBD_RCTRL (KBD_EXT | 0x14)
|
||||||
|
#define KBD_RALT (KBD_EXT | 0x11)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+28
-13
@@ -3,25 +3,40 @@
|
|||||||
*
|
*
|
||||||
* Аппаратный приёмный FIFO SIO — 3 байта. Если прерывания запрещены
|
* Аппаратный приёмный FIFO SIO — 3 байта. Если прерывания запрещены
|
||||||
* достаточно долго (DI-окна графики/кадрового IRQ), а в этот момент
|
* достаточно долго (DI-окна графики/кадрового IRQ), а в этот момент
|
||||||
* приходит пачка скан-кодов (напр. одновременное отпускание shift+стрелка
|
* приходит пачка скан-кодов (напр. быстрый тап стрелки = E0 74 + E0 F0 74,
|
||||||
* = F0 12 E0 F0 74, 5 байт) — FIFO переполняется, байт(ы) теряются
|
* 5 байт) — FIFO переполняется, байт(ы) теряются аппаратно. Потерянный
|
||||||
* аппаратно. Потерянный break → клавиша «залипает» в _kbdraw_down.
|
* break → клавиша «залипает» в _kbdraw_down.
|
||||||
*
|
*
|
||||||
* Трамплин ловит overrun (RR1 бит5) и взводит _kbdraw_overrun. Приложение
|
* Трамплин ловит overrun (RR1 бит5) и взводит _kbdraw_overrun. Приложение
|
||||||
* зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывает ВСЁ
|
* зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывается
|
||||||
* held-состояние (какой именно break потерян — неизвестно). Реально
|
* held-состояние ВСЕХ клавиш, КРОМЕ модификаторов (Shift/Ctrl/Alt —
|
||||||
* зажатые клавиши «перечитаются» их make-повтором/следующим нажатием.
|
* таблица ниже). Обычные реально зажатые клавиши «перечитаются» их
|
||||||
*
|
* make-повтором (typematic) за ~0.1 с. Модификаторы НЕ сбрасываем
|
||||||
* ВНИМАНИЕ: путь overrun-recovery НЕ проверен детерминированно (баг
|
* принципиально: PS/2 автоповторяет только ПОСЛЕДНЮЮ нажатую клавишу,
|
||||||
* интермиттентный, зависит от тайминга DI) — требует проверки на железе.
|
* удерживаемый Shift при повторе стрелки не шлёт НИЧЕГО — сброшенный
|
||||||
|
* модификатор невозможно перечитать, он «отваливался» до перенажатия
|
||||||
|
* (баг PoP: Shift+→ = осторожный шаг превращался в бег, 2026-07-22).
|
||||||
|
* Цена: если overrun потерял именно break модификатора, тот залипнет до
|
||||||
|
* следующего нажатия — редкий случай против регулярного отвала.
|
||||||
*/
|
*/
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "_kbdraw.h"
|
#include "_kbdraw.h"
|
||||||
|
|
||||||
|
/* Скан-коды модификаторов (PS/2 Set 2): L/R Shift, LCtrl, LAlt,
|
||||||
|
* RCtrl (E0 14 → +256), RAlt (E0 11 → +256). */
|
||||||
|
static const uint16_t _kbd_mod_codes[6] = {
|
||||||
|
0x12, 0x59, 0x14, 0x11, 0x0114, 0x0111
|
||||||
|
};
|
||||||
|
|
||||||
void kbd_raw_sync(void)
|
void kbd_raw_sync(void)
|
||||||
{
|
{
|
||||||
if (_kbdraw_overrun) {
|
uint8_t save[6];
|
||||||
_kbdraw_overrun = 0;
|
uint8_t i;
|
||||||
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
|
|
||||||
}
|
if (!_kbdraw_overrun) return;
|
||||||
|
_kbdraw_overrun = 0;
|
||||||
|
|
||||||
|
for (i = 0; i < 6; i++) save[i] = _kbdraw_down[_kbd_mod_codes[i]];
|
||||||
|
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
|
||||||
|
for (i = 0; i < 6; i++) _kbdraw_down[_kbd_mod_codes[i]] = save[i];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user