diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index ac3edbf..3dce59f 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -57,6 +57,23 @@ const uint8_t n_banks = 4; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; +/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — НЕ ЧИНИТСЯ ИЗ ЭТОГО ЦИКЛА. + * + * Здесь стояли шесть вызовов kbd_raw_poll() (вычерпать FIFO клавиатуры + * после каждой тяжёлой фазы кадра). Убраны: замерено, что они не меняют + * НИЧЕГО — 9 дошедших make-байт из 10 нажатий и с ними, и без них. Они + * стояли ровно там, где прерывания и так разрешены, то есть добавляли то, + * что трамплин делает сам. + * + * Настоящая причина лежит НИЖЕ нашего кода: при зажатом Shift PS/2 шлёт на + * расширенную клавишу вдвое больше байт («fake shift»), а импульс запроса + * прерывания пропускается примерно в 44 % случаев — байты копятся в + * трёхбайтовом FIFO SIO и один из полусотни теряется ДО чтения порта. + * Длина наших DI-окон ни при чём: в замороженном кадре, где блитов нет + * вообще, потерь БОЛЬШЕ, а не меньше. + * + * Полный протокол замеров и куда копать дальше — TASKS.md, KBD-1. */ + /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ #define SEQ_STAND 2 #define ROOM_1 1 diff --git a/docs/TODO.md b/docs/TODO.md index ae6b921..ad01e12 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -279,15 +279,32 @@ Quick wins: - [ ] **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), но - аккорд «держу →, тапнул ↑» остаётся уязвим. + E0 74 + E0 F0 74 при FIFO 3) даёт overrun. + **Уточнение 2026-08-01 (по коду драйвера, прежняя гипотеза + НЕВЕРНА):** dev-MAME per-byte INT ДАЁТ — + `mame/sources/MAME/src/mame/sinclair/sprinter.cpp` + `on_kbd_data()` ставит `m_irqs->in_set<1>()` на каждый принятый + байт. Но тут же заводит `m_irq_off_timer` на **32 такта CPU**, и + `irq_off()` снимает линию — то есть импульс, пришедшийся на наше + DI-окно, теряется НАСОВСЕМ (байт остаётся в FIFO до следующего + IRQ или кадрового 50 Гц). А DI-окна у нас длинные: ядра + акселератора держат `di` на весь блит (`libbgi/bgi256/ + _bgi_blit_cols_raw.c`), это сотни микросекунд против 32 тактов. + **Замерено 2026-08-01 (PoP roomtest, счётчики в MAME): обе + «наши» гипотезы отпали.** (а) `kbd_raw_poll()` из главного цикла + не меняет ничего (9/10 с ним и без); (б) снятие `di` в accel-ядрах + УРОНИЛО машину — режим «акселератор при EI» тут недоступен; и сама + длина DI ни при чём (в замороженном кадре без блитов потерь + БОЛЬШЕ). Байт теряется ДО чтения порта: на 49 прочитанных байт + только 28 входов в клавиатурную ветку, т.е. ~44 % импульсов + запроса не обслужены и 3-байтовый FIFO переполняется. Главный + подозреваемый — `m_irq_off_timer` в sprinter.cpp: он ОДИН на два + источника (экран + клавиатура), `irq_off()` гасит обе линии, так + что кадровое прерывание способно обрезать клавиатурный импульс. + Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1. + Recovery-политика уже терпима к частым overrun'ам (селективный + wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и + «держу Shift, тапаю ←» остаются уязвимы. ## Known quirks (зафиксированы, обходы в libc) diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 0c0d9a4..f5743e3 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -313,6 +313,7 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс | `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) | +| `uint8_t kbd_raw_poll(void)` | вычерпать FIFO ОПРОСОМ, не дожидаясь прерывания: 0 — было пусто (~40 тактов), 1 — что-то декодировано. Звать МЕЖДУ фазами кадра, после тяжёлых блитов — не раз в кадр (см. ниже, «почему одного прерывания мало») | | `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 | @@ -335,6 +336,32 @@ git как урок: при повторных «нет эффекта» на с брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться только на визуальный снимок с произвольным таймингом. +**ПОЧЕМУ ОДНОГО ПРЕРЫВАНИЯ МАЛО (`kbd_raw_poll`, 2026-08-01).** Приёмный +FIFO SIO — 3 байта, и рассчитывать на «каждый байт разбудит нас» нельзя: +запрос прерывания клавиатуры держится единицы микросекунд (в dev-MAME — +32 такта CPU, `sprinter.cpp` `on_kbd_data` → `irq_off_timer`), а ядра +акселератора держат `DI` на весь блит — сотни микросекунд. Импульс, +попавший в такое окно, теряется насовсем; байт лежит в FIFO до следующего +прерывания (следующий байт либо кадровое, 50 Гц). Трафик же идёт пачками: +тап стрелки = 5 байт, а при УДЕРЖИВАЕМОМ Shift PS/2 обрамляет расширенный +код «фиктивным шифтом» (`E0 F0 12` … `E0 12`) — по 5 байт и на нажатие, и +на отпускание. Три ячейки такую пачку не держат: потерянный make = «нажатие +не сработало», потерянный break = залипание. Отсюда `kbd_raw_poll()` — +вычерпывание тем же декодером, но из главного цикла. Тело идёт под `DI` +(чтение порта 0x18 деструктивно, а read-modify-write карты гонится с +трамплином) и БЕЗУСЛОВНО делает `EI` на выходе — из ISR звать нельзя. + +**Замер эффективности (2026-08-01, PoP roomtest — читать до применения!).** +Расстановка «несколько вызовов за кадр, после тяжёлых фаз» **не даёт +ничего**: 9 дошедших make-байт из 10 нажатий и с ней, и без неё. Причина — +такие вызовы попадают ровно в участки с разрешёнными прерываниями и лишь +дублируют трамплин. Там же измерено, что длина DI-окон графики на потери +НЕ влияет (в кадре вообще без блитов потерь больше), а теряется байт ДО +чтения порта: примерно 44 % импульсов запроса прерывания не обслуживаются, +и трёхбайтовый FIFO переполняется. Полный протокол и куда копать — +`applications/PoP/roomtest/TASKS.md`, задача KBD-1. Смысл может быть +только у ПЛОТНОГО опроса (в цикле ожидания кадра), это ещё не проверено. + **ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/ kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт то же самое, что было на момент открытия — резидентный обработчик DSS, diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h index 9f78e0c..e359769 100644 --- a/libc/include/kbd_raw.h +++ b/libc/include/kbd_raw.h @@ -57,6 +57,30 @@ uint8_t kbd_raw_down(uint16_t code); * «отваливался» при каждом overrun (см. kbd_raw_sync.c). */ void kbd_raw_sync(void); +/* Вычерпать приёмный FIFO ОПРОСОМ, не дожидаясь прерывания. Возвращает + * 0 (FIFO был пуст, ~40 тактов) или 1 (что-то вычерпано и декодировано в + * ту же карту, что ведёт трамплин). + * + * Зачем: на каждый принятый байт запрос прерывания живёт единицы + * микросекунд, и пропущенный импульс не «догоняется» — байт лежит в + * 3-байтовом FIFO до следующего прерывания. Пачки в 5 байт (тап стрелки; + * при зажатом Shift — ещё и «fake shift» вокруг расширенного кода) при + * этом переполняют FIFO: теряется make («нажатие не сработало») или break + * (залипание). + * + * ЧЕСТНАЯ ОГОВОРКА ПО ЭФФЕКТИВНОСТИ (замер 2026-08-01, PoP roomtest, + * см. applications/PoP/roomtest/TASKS.md, KBD-1): расстановка «несколько + * вызовов за кадр, после тяжёлых фаз» НЕ ДАЁТ НИЧЕГО — потери те же, что + * без неё. Такие вызовы попадают в участки, где прерывания и так + * разрешены, и лишь дублируют трамплин. Смысл появляется только у + * ПЛОТНОГО опроса (в цикле ожидания, десятки-сотни раз за кадр), и это + * ещё не подтверждено замером. Не ставить эту функцию в игровой цикл + * «на всякий случай» — сначала померить. + * + * Тело идёт под DI и БЕЗУСЛОВНО делает EI на выходе: рассчитано на вызов + * из главного цикла, из ISR звать нельзя. */ +uint8_t kbd_raw_poll(void); + #define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */ /* Позиционные коды PS/2 Set 2 — то, что реально нужно платформеру. diff --git a/libc/kbd/kbd_raw_poll.c b/libc/kbd/kbd_raw_poll.c new file mode 100644 index 0000000..e2398d7 --- /dev/null +++ b/libc/kbd/kbd_raw_poll.c @@ -0,0 +1,145 @@ +/* + * kbd_raw_poll — вычерпать приёмный FIFO клавиатуры ОПРОСОМ, не дожидаясь + * прерывания. Дополняет (не заменяет) декодер в трамплине: тот же разбор + * PS/2 Set 2, та же битовая карта _kbdraw_down[], тот же FSM префиксов + * _kbdraw_pending — просто вызванный из главного цикла приложения. + * + * ЗАЧЕМ (разбор 2026-08-01, applications/PoP/roomtest/TASKS.md, KBD-1). + * Приёмный FIFO SIO — 3 байта, и рассчитывать на то, что каждый принятый + * байт разбудит нас прерыванием, НЕЛЬЗЯ: + * - импульс запроса прерывания клавиатуры живёт лишь 32 такта CPU + * (dev-MAME: sprinter.cpp on_kbd_data → irq_off_timer), а ядра + * акселератора держат DI на весь блит — сотни микросекунд. Импульс, + * попавший в такое окно, теряется НАСОВСЕМ: байт остаётся в FIFO до + * следующего прерывания (следующий байт либо кадровое, 50 Гц); + * - трафик от клавиатуры пачками: тап стрелки — 5 байт, а при зажатом + * Shift ещё и «fake shift» (E0 F0 12 / E0 12 вокруг расширенного кода) + * — по 5 байт на нажатие И на отпускание. Три ячейки FIFO такую пачку + * не держат: потерянный make = «нажатие не сработало», потерянный + * break = залипшая клавиша (её потом лечит kbd_raw_sync, ценой сброса + * всех немодификаторных клавиш). + * Опрос из главного цикла снимает зависимость от пойманного импульса: чем + * чаще позвали, тем меньше шанс, что в FIFO успеет накопиться четвёртый + * байт. Звать между фазами кадра (после тяжёлых блитов), НЕ раз в кадр. + * + * ABI: __sdcccall(1), без аргументов, результат uint8 в A — 0 если FIFO + * был пуст (быстрый выход, ~40 тактов), 1 если что-то вычерпано. IX не + * трогаем. Тело идёт под DI: чтение порта 0x18 деструктивно, и гонка с + * трамплином за read-modify-write ячейки _kbdraw_down[] стоила бы + * потерянного бита. DI/EI — БЕЗУСЛОВНЫЕ (как IRQ_DISABLE/IRQ_ENABLE в + * kbd_raw_open): функция рассчитана на вызов из главного цикла, где + * прерывания разрешены; звать её из ISR нельзя. + */ + +#include +#include "_kbdraw.h" +#include "../irq/_irq.h" + +uint8_t kbd_raw_poll(void) __naked +{ + __asm + ld a, (__kbdraw_active) + or a, a + jr Z, kp_off ; raw-канал закрыт — не наше дело + ;; DI ДО пробы «есть ли байт»: иначе между пробой и DI успело бы + ;; влезть прерывание, трамплин вычерпал бы FIFO, а мы всё равно + ;; пошли бы читать порт 0x18 — и получили бы из пустого приёмника + ;; мусорный «скан-код» (взведённый бит клавиши, которую не нажимали). + di + in a, (#_IRQ_SIO_A) ; SIO-A RR0 + rrca ; бит 0 — байт принят? + jr C, kp_work + ei + kp_off: + xor a, a + ret + + kp_work: + push hl + push de + push bc + kp_drain: + in a, (#_IRQ_SIO_DATA) ; чтение деструктивно = дренаж байта + cp #0xF0 + jr Z, kp_setbrk + cp #0xE0 + jr Z, kp_setext + + ;; --- обычный байт кода: БИТ code&7 в _kbdraw_down[code>>3]. + ;; Маска сдвигом, а не таблицей — ровно как в трамплине, чтобы + ;; расхождения декодеров были невозможны by construction. --- + ld e, a ; E = код + and #7 + ld b, a + inc b ; B = (код&7)+1 для djnz + ld a, #1 + jr kp_mskdec + kp_mskloop: + add a, a + kp_mskdec: + djnz kp_mskloop ; A = 1 << (код&7) + ld c, a ; C = маска бита + ld a, e + rrca + rrca + rrca + and #0x1F ; A = код>>3 (код <= 255) + ld hl, #__kbdraw_down + ld d, #0 + ld e, a + add hl, de + ld a, (__kbdraw_pending) + bit 1, a ; _KBDRAW_PEND_EXT? + jr Z, kp_noext + ld de, #32 ; +256 кодов = +32 байта карты + add hl, de + kp_noext: + ld a, (__kbdraw_pending) + bit 0, a ; _KBDRAW_PEND_BREAK? + jr Z, kp_mkdown + ld a, c + cpl ; break → сбросить бит + and a, (hl) + jr kp_wr + kp_mkdown: + ld a, c ; make → взвести бит + or a, (hl) + kp_wr: + ld (hl), a + xor a, a + ld (__kbdraw_pending), a ; префиксы потреблены + jr kp_next + + kp_setbrk: + ld hl, #__kbdraw_pending + set 0, (hl) + jr kp_next + kp_setext: + ld hl, #__kbdraw_pending + set 1, (hl) + kp_next: + in a, (#_IRQ_SIO_A) + rrca ; ещё байт в FIFO? + jr C, kp_drain + + ;; --- FIFO пуст: тот же разбор Rx-overrun, что и в трамплине. + ;; RR1 бит5 = Rx Overrun; без Error Reset (WR0 = 0x30) бит залипнет + ;; и следующий overrun будет неотличим. --- + ld a, #1 + out (#_IRQ_SIO_A), a ; указатель на RR1 + in a, (#_IRQ_SIO_A) ; читаем RR1 (указатель авто-сброс в 0) + bit 5, a + jr Z, kp_noovr + ld a, #0x30 + out (#_IRQ_SIO_A), a ; WR0 Error Reset + ld a, #1 + ld (__kbdraw_overrun), a ; снимет kbd_raw_sync() + kp_noovr: + pop bc + pop de + pop hl + ei + ld a, #1 + ret + __endasm; +} diff --git a/tests/kbdpoll/Makefile b/tests/kbdpoll/Makefile new file mode 100644 index 0000000..8eee5f1 --- /dev/null +++ b/tests/kbdpoll/Makefile @@ -0,0 +1,5 @@ +# Build kbdpoll.exe — потолок «плотного опроса» клавиатуры (KBD-1). + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := kbdpoll +include $(PROJ_ROOT)/app.mk diff --git a/tests/kbdpoll/kbdpoll.c b/tests/kbdpoll/kbdpoll.c new file mode 100644 index 0000000..918f86d --- /dev/null +++ b/tests/kbdpoll/kbdpoll.c @@ -0,0 +1,55 @@ +/* + * kbdpoll — измерение ПОТОЛКА «плотного опроса» клавиатуры. + * + * Вопрос, на который отвечает тест (applications/PoP/roomtest/TASKS.md, + * KBD-1): при зажатом Shift часть нажатий расширенных клавиш теряется — + * байт не доезжает до порта 0x18, потому что импульс запроса прерывания + * пропускается примерно в 44 % случаев и трёхбайтовый FIFO SIO + * переполняется. Редкий опрос (несколько раз за кадр) не помог вовсе. + * Здесь опрос идёт МАКСИМАЛЬНО плотно — вся программа только этим и + * занята, — то есть измеряется ЛУЧШЕЕ, чего опрос в принципе может + * достичь. Если и здесь байты теряются, приём не спасёт и в игре, и + * искать надо в эмуляции/железе, а не у нас. + * + * Как мерить (MAME, счётчики брейкпоинтами, машина не останавливается): + * 1. Найти в .map адреса `_kbd_raw_poll` и хвоста карты `__kbdraw_down`. + * 2. bpset <адрес чтения порта 0x18 внутри kbd_raw_poll>,1,{ b@СЧ1 = b@СЧ1+1 ; g } + * bpset <адрес установки make-бита>,E==0x6C && (b@_kbdraw_pending & 1)==0, + * { b@СЧ2 = b@СЧ2+1 ; g } + * 3. Держать Left Shift, нажать Home ровно N раз + * (`:kbd:ms_naturl:P1.7` 0x0002 — Shift, `:kbd:ms_naturl:P2.5` 0x0001 — Home). + * Home выбран потому, что он расширенный (тот же «fake shift», что у + * стрелок), но в этой программе ничего не делает. + * 4. Ожидание: байт ровно 10*N (нажатие E0 F0 12 + E0 6C, отпускание + * E0 F0 6C + E0 12), make'ов ровно N. + * + * Итог теста на экран не выводится СПЕЦИАЛЬНО: любой вывод — это работа, + * которая разрежает опрос и портит замер. Числа снимаются из памяти. + * + * Выход — ESC. Чтобы отличить «программа жива» от «повисла», крутится + * бордюр: цвет меняется раз в 4096 опросов (медленное мерцание = работаем). + */ + +#include +#include + +__sfr __at (0xFE) io_border; + +int main(void) +{ + uint16_t spin = 0; + + if (kbd_raw_open() != 0) + return 1; + + /* Ничего, кроме опроса. Ни vsync, ни графики, ни ввода-вывода — + * иначе замеряли бы не потолок приёма, а свою же нагрузку. */ + while (!kbd_raw_down(KBD_ESC)) { + kbd_raw_poll(); + if (++spin == 0) + io_border ^= 4; /* признак жизни: бит 2 (зелёный) */ + } + + kbd_raw_close(); + return 0; +}