Files
Sprinter-SDCC/libc/include/kbd_raw.h
T
snark13 4e43890fce SprPoP: финал больше не убивает программу — прямой вызов в чужой банк
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (di;halt на 0x0000) либо уходила
в reset.  Одинаково из Flex Navigator и из голого DSS.

КОРЕНЬ.  pop_ui.h объявлял группу pop_text_*_mapped БЕЗ __banked.  Пока
pop_hof.c лежал в банке 9 рядом с pop_ui.c, прямой call был верен; после
переноса pop_hof/pop_config/pop_pal в банк 10 тот же call стал уходить в
пустой хвост чужого банка.  Процессор полз по 0xFF до 0x0000, где ловушка
DSS ставит B=0x27 и сворачивает процесс — подмена страниц W1/W2/W3,
которую было видно на трупе, оказалась уборкой, а не причиной.

Точную инструкцию (call $E503 = _pop_text_map банка 9) дала трассировка
MAME на узком участке: trace включалась брейкпоинтом на входе в
pop_hof_show и выключалась на процедуре завершения процесса DSS (0x1E56).

ЧТО СДЕЛАНО

* pop_ui.h/.c — группа text_*_mapped помечена __banked.
* toolchain/check_bank_calls.py — две проверки банкового кода:
  1) прямой call в чужой банк (доказательна, ВАЛИТ сборку — проверено
     намеренной поломкой);
  2) указатель на данные своего банка, отданный в чужой (эвристика по
     форме кода, только предупреждает).
  Встроена в app.mk, запускается сразу после линковки.
* pop_hof.c — курсор ввода строится на стеке: литерал "_" лежал в _BANK10
  и после пометки __banked уезжал из-под ног чужому банку, заливая экран
  знаками вопроса.
* libc: kbd_raw_keypad_as_ext() — kbd_raw_sync переносит голые коды
  нумпада в EXT-половину карты.  Лечит залипание стрелок (потерянный
  префикс E0 сажал make в PLAIN как код нумпада, и снять его было нечем),
  заодно нумпад стал управлением: 7/8/9, 4/6, 2 и 5 = вниз.
* pop_pace.c — цикл ожидания луча зовёт тот же idle-хук, что и
  gfx_wait_vsync: без этого F10 в геймплее не работал вовсе.
* pop_hof.c — Esc в таблице рекордов отменяет запись (расхождение с
  оригиналом записано в docs/impl_diff.md).
* Экран версии показывается только через Menu/Settings/About: стартовый
  показ и Ctrl+V убраны, мёртвый код снят.
* sprpop_cold.c — pop_start_level зовёт pop_hp_invalidate: после Ctrl+A с
  выросшим за уровень максимумом полоса HP моргала между страницами.

Разбор всех четырёх багов — в applications/PoP/roomtest/BUGS_CLOSED.md
(FINAL-BANKCALL, FINAL-HOF-GARBAGE, KBD-ARROW-PHANTOM, F10-GAMEPLAY),
правило про банки — в applications/SprPoP/CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:11:01 +03:00

137 lines
9.6 KiB
C
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.
/*
* kbd_raw.h — эксклюзивный raw-канал клавиатуры: держит битовую карту
* «клавиша N зажата ПРЯМО СЕЙЧАС», обновляемую напрямую из
* прерывания (декодирует PS/2 Scan Code Set 2 — make/break, тот же
* протокол, что и в docs/samples/sprinterKeybLib.asm).
*
* Прецедент и мотивация — applications/PoP/docs/PORT_PLAN.md §2:
* ESTEX kbhit/getch/getkey/CTRLKEY — событийные (нажатие, без
* release), обычные клавиши (не модификаторы) вообще не имеют
* live-state в ESTEX; трамплин кадрового прерывания
* (libc/irq/_irq_tramp.c) в штатном режиме отдаёт клавиатурные байты
* ПРЯМО в DSS, не читая порт данных — байт из аппаратного приёмного
* регистра можно прочитать только ОДИН раз, поэтому «подсмотреть, не
* мешая DSS» невозможно технически.
*
* ЖИЗНЕННЫЙ ЦИКЛ И ГЛАВНОЕ СЛЕДСТВИЕ: пока kbd_raw_open() активен, ВЕСЬ
* поток клавиатурных байт достаётся ЭТОМУ модулю — DSS их не видит.
* kbhit/getch/getkey/CTRLKEY (в т.ч. kbd_mod_state) НЕ получают новых
* событий, пока raw-канал открыт. Приложение обязано проверять «выход»
* (например ESC — KBD_ESC) через kbd_raw_down() само. Перед экранами,
* которым нужен обычный ESTEX-ввод (диалоги/меню на консольном I/O),
* закрыть канал kbd_raw_close(). Паттерн один в один как у
* cbl_open()/cbl_close() (<cbl.h>) — тот же приватный IM2-хук, не
* чейнящийся к DSS.
*
* КОДЫ КЛАВИШ: значения ниже — общеизвестный стандарт AT/PS-2 Scan
* Code Set 2 (не специфика Sprinter). TODO до использования в PoC:
* подтвердить в MAME, что реальный поток байт с SIO-A клавиатуры
* Sprinter именно этот набор кодов (см. defer_unexplained_quirks).
*/
#ifndef KBD_RAW_H
#define KBD_RAW_H
#include <stdint.h>
/* Включить raw-канал: 0 / -1+errno (EBUSY — уже открыт; прочие коды —
* см. errno.h, как у irq_install/cbl_open — общий IM2-механизм). */
int kbd_raw_open(void);
/* Выключить raw-канал, вернуть клавиатуру DSS. Идемпотентно; висит
* на atexit (страховка, как у cbl_close). */
void kbd_raw_close(void);
/* Зажата ли клавиша code ПРЯМО СЕЙЧАС (0/1). code вне 0..511 — 0
* (защитно). Для расширенных клавиш (стрелки и т.п., префикс 0xE0
* на проводе) прибавить KBD_EXT к базовому коду. */
uint8_t kbd_raw_down(uint16_t code);
/* Считать ГОЛЫЕ (без префикса 0xE0) коды нумпада ТЕМИ ЖЕ клавишами, что и
* расширенные: 0x69..0x7D, кроме ESC 0x76 и F11 0x78. По умолчанию
* выключено — нумпад остаётся отдельным набором клавиш.
*
* В PS/2 это и есть одни и те же физические клавиши: навигационный блок
* шлёт код с префиксом 0xE0, нумпад — тот же код голым (KP4 = Left,
* KP8 = Up, KP7 = Home, KP9 = PgUp и т.д.). Флаг восстанавливает это
* равенство: kbd_raw_sync() переносит биты голых кодов в EXT-половину
* карты и гасит их в PLAIN.
*
* ЗАЧЕМ ЭТО НУЖНО (а не только «нумпад тоже работает»). При переполнении
* 3-байтового FIFO SIO теряется префикс, и make стрелки садится в PLAIN
* как код нумпада — а break придёт уже с префиксом и снимет бит в ДРУГОЙ
* половине. Клавиша остаётся зажатой НАВСЕГДА: kbd_raw_any_down()
* отвечает «да», и любое ожидание «пока ничего не нажато» виснет насмерть.
* После перекладки make и break работают с одним и тем же битом.
*
* Чего флаг НЕ лечит: потерю префикса у BREAK (`E0 F0 6B` -> `F0 6B`) —
* тогда бит стоит уже в EXT-половине и выглядит как реально зажатая
* клавиша; снимается перенажатием или typematic-повтором.
*
* Перекладку делает kbd_raw_sync(), то есть приложение обязано звать его
* раз в кадр (оно и так обязано — см. ниже). */
void kbd_raw_keypad_as_ext(uint8_t on);
/* Есть ли хотя бы одна зажатая клавиша (0/1). Полезно для экранов с
* семантикой «продолжить любой клавишей»; один вызов читает 64-байтную
* bitmap, а не перебирает все 512 scan-кодов через kbd_raw_down(). */
uint8_t kbd_raw_any_down(void);
/* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения
* kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка
* скан-кодов (напр. быстрый тап стрелки: 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);
/* Вычерпать приёмный FIFO ОПРОСОМ, не дожидаясь прерывания. Возвращает
* 0 (FIFO был пуст, ~40 тактов) или 1 (что-то вычерпано и декодировано в
* ту же карту, что ведёт трамплин).
*
* Зачем: на каждый принятый байт запрос прерывания живёт единицы
* микросекунд, и пропущенный импульс не «догоняется» — байт лежит в
* 3-байтовом FIFO до следующего прерывания. Пачки (тап стрелки — 5 байт;
* одновременное отпускание нескольких клавиш — больше) при этом
* переполняют FIFO: теряется make («нажатие не сработало») или break
* (залипание).
*
* КАК ЕЁ ЗВАТЬ (замеры 2026-08-01, PoP roomtest — см.
* applications/PoP/roomtest/TASKS_CLOSED.md, KBD-1):
* - «несколько вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО —
* потери те же, что без них: такие вызовы попадают в участки, где
* прерывания и так разрешены, и лишь дублируют трамплин;
* - работает только ПЛОТНЫЙ опрос, порядка раза в 0.5 мс. Столько
* времени есть даром в ожидании кадра, поэтому штатный способ —
* повесить эту функцию idle-хуком графики:
* gfx_set_idle_hook(my_poll_wrapper); // <gfx.h>
* Проверено: 35 нажатий стрелки с зажатым Shift → 35 дошедших make
* против 9 из 10 без хука.
* Не ставить в игровой цикл «на всякий случай» — сначала померить.
*
* Тело идёт под DI и БЕЗУСЛОВНО делает EI на выходе: рассчитано на вызов
* из главного цикла, из ISR звать нельзя. */
uint8_t kbd_raw_poll(void);
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
/* Позиционные коды PS/2 Set 2 — то, что реально нужно платформеру.
* TODO: подтвердить в MAME перед PoC (см. предупреждение выше). */
#define KBD_UP (KBD_EXT | 0x75)
#define KBD_DOWN (KBD_EXT | 0x72)
#define KBD_LEFT (KBD_EXT | 0x6B)
#define KBD_RIGHT (KBD_EXT | 0x74)
#define KBD_SPACE 0x29
#define KBD_ENTER 0x5A
#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