4e43890fce
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (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>
137 lines
9.6 KiB
C
137 lines
9.6 KiB
C
/*
|
||
* 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
|