From 3d586af031f0460da271f4812e91aedd5b52858e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 23 Jul 2026 19:38:12 +0300 Subject: [PATCH] =?UTF-8?q?libc/kbd:=20raw-=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=80=D0=B0=20=E2=80=94=20=D0=B2=D1=8B=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=BF=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20FIFO?= =?UTF-8?q?=20+=20=D1=81=D0=B5=D0=BB=D0=B5=D0=BA=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20wipe=20=D0=BC=D0=BE=D0=B4=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/TODO.md | 12 ++++ docs/kbd-games.md | 133 ++++++++++++++++++++++++++++++++++++++++ docs/libc-reference.md | 2 + libc/include/kbd_raw.h | 14 ++++- libc/kbd/kbd_raw_sync.c | 41 +++++++++---- 5 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 docs/kbd-games.md diff --git a/docs/TODO.md b/docs/TODO.md index 74c8dc5..ae6b921 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -276,6 +276,18 @@ Quick wins: CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков, которые должны были обслужить CBL-насос, уходят мимо, кольцо недоливается. Нужно исследование (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) diff --git a/docs/kbd-games.md b/docs/kbd-games.md new file mode 100644 index 0000000..b9be025 --- /dev/null +++ b/docs/kbd-games.md @@ -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-канал ``. + +## Матчасть: как клавиатура устроена снизу + +- 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_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. diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 18bd895..0c0d9a4 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -312,8 +312,10 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс | `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) | | `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit | | `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_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 (был неверный вывод, ниже — то, что реально подтвердилось):** изначально скриншот-тестирование (снимок экрана diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h index 89e84a8..9f78e0c 100644 --- a/libc/include/kbd_raw.h +++ b/libc/include/kbd_raw.h @@ -48,9 +48,13 @@ uint8_t kbd_raw_down(uint16_t code); /* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения * kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка - * скан-кодов (напр. shift+стрелка release) переполняет его → потерян break - * → залипшая клавиша. Трамплин ловит overrun и взводит флаг; kbd_raw_sync - * по флагу сбрасывает ВСЁ held-состояние (реально зажатые перечитаются). */ + * скан-кодов (напр. быстрый тап стрелки: make+break = 5 байт) переполняет + * его → потерян break → залипшая клавиша. Трамплин ловит overrun и + * взводит флаг; kbd_raw_sync по флагу сбрасывает held-состояние всех + * клавиш КРОМЕ модификаторов (Shift/Ctrl/Alt): PS/2 автоповторяет только + * последнюю нажатую клавишу, поэтому обычные зажатые перечитаются + * typematic'ом, а сброшенный модификатор восстановить нечем — он бы + * «отваливался» при каждом overrun (см. kbd_raw_sync.c). */ void kbd_raw_sync(void); #define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */ @@ -66,5 +70,9 @@ void kbd_raw_sync(void); #define KBD_ESC 0x76 #define KBD_LSHIFT 0x12 #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 diff --git a/libc/kbd/kbd_raw_sync.c b/libc/kbd/kbd_raw_sync.c index 733dd6e..c3117d5 100644 --- a/libc/kbd/kbd_raw_sync.c +++ b/libc/kbd/kbd_raw_sync.c @@ -3,25 +3,40 @@ * * Аппаратный приёмный FIFO SIO — 3 байта. Если прерывания запрещены * достаточно долго (DI-окна графики/кадрового IRQ), а в этот момент - * приходит пачка скан-кодов (напр. одновременное отпускание shift+стрелка - * = F0 12 E0 F0 74, 5 байт) — FIFO переполняется, байт(ы) теряются - * аппаратно. Потерянный break → клавиша «залипает» в _kbdraw_down. + * приходит пачка скан-кодов (напр. быстрый тап стрелки = E0 74 + E0 F0 74, + * 5 байт) — FIFO переполняется, байт(ы) теряются аппаратно. Потерянный + * break → клавиша «залипает» в _kbdraw_down. * * Трамплин ловит overrun (RR1 бит5) и взводит _kbdraw_overrun. Приложение - * зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывает ВСЁ - * held-состояние (какой именно break потерян — неизвестно). Реально - * зажатые клавиши «перечитаются» их make-повтором/следующим нажатием. - * - * ВНИМАНИЕ: путь overrun-recovery НЕ проверен детерминированно (баг - * интермиттентный, зависит от тайминга DI) — требует проверки на железе. + * зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывается + * held-состояние ВСЕХ клавиш, КРОМЕ модификаторов (Shift/Ctrl/Alt — + * таблица ниже). Обычные реально зажатые клавиши «перечитаются» их + * make-повтором (typematic) за ~0.1 с. Модификаторы НЕ сбрасываем + * принципиально: PS/2 автоповторяет только ПОСЛЕДНЮЮ нажатую клавишу, + * удерживаемый Shift при повторе стрелки не шлёт НИЧЕГО — сброшенный + * модификатор невозможно перечитать, он «отваливался» до перенажатия + * (баг PoP: Shift+→ = осторожный шаг превращался в бег, 2026-07-22). + * Цена: если overrun потерял именно break модификатора, тот залипнет до + * следующего нажатия — редкий случай против регулярного отвала. */ #include #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) { - if (_kbdraw_overrun) { - _kbdraw_overrun = 0; - memset(_kbdraw_down, 0, sizeof(_kbdraw_down)); - } + uint8_t save[6]; + uint8_t i; + + 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]; }