diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c index e24a1d3..45db979 100644 --- a/libc/irq/_irq_tramp.c +++ b/libc/irq/_irq_tramp.c @@ -46,12 +46,12 @@ void _irq_tramp(void) __naked push af ; сохранить прерванный AF in a, (#_IRQ_SIO_A) rrca ; бит 0 — клавиатурный байт принят? - jr NC, tr_notkbd + jr NC, tr_hub_notkbd ; не клавиатура (через ретранслятор — см. ниже) ;; --- клавиатура --- ld a, (__kbdraw_active) or a, a - jr Z, tr_kbd_dss ; raw выключен — прежнее поведение + jr Z, tr_hub_dss ; raw выключен — прежнее поведение ;; --- raw включён: байт наш целиком, DSS его не увидит --- ;; ВЫЧЕРПЫВАЕМ FIFO SIO В ЦИКЛЕ (как эталон keybIntHandler / @@ -63,18 +63,85 @@ void _irq_tramp(void) __naked push hl push de push bc ; маска бита строится в BC (см. ниже) + ;; РАСКЛАДКА ЦИКЛА. Все переходы обязаны быть jr (см. шапку: + ;; COPY-SAFE), а тело выросло за 127 байт — поэтому префиксные + ;; обработчики стоят СРАЗУ за своими cp, а возврат на начало цикла + ;; идёт через ретранслятор tr_kbd_hub посередине тела. tr_kbd_drain: in a, (#_IRQ_SIO_DATA) ; читаем — тем самым дренируем байт cp #0xF0 - jr Z, tr_kbd_setbrk + jr NZ, tr_kbd_nF0 + ld hl, #__kbdraw_pending + set 0, (hl) + jr tr_kbd_hub + tr_kbd_nF0: cp #0xE0 - jr Z, tr_kbd_setext + jr NZ, tr_kbd_nE0 + ld hl, #__kbdraw_pending + set 1, (hl) + jr tr_kbd_hub + tr_kbd_nE0: + + ;; --- «fake shift»: расширенные коды 12/59 — не клавиши, а обёртка, + ;; которой клавиатура сопровождает КАЖДЫЙ расширенный код, пока + ;; реально зажат Shift (см. _kbdraw.h). Отсюда состояние Shift + ;; определяется в обе стороны и не зависит от целости потока. --- + ld e, a ; E = код + ld a, (__kbdraw_pending) + bit 1, a ; EXT? + ld a, e ; A = код обратно (ld флаги не трогает) + jr Z, tr_kbd_code ; обычный код — прежний путь + cp #0x12 + jr Z, tr_kbd_fake + cp #0x59 + jr Z, tr_kbd_fake + ;; расширенная КЛАВИША: make без обёртки => Shift не зажат + ld a, (__kbdraw_pending) + bit 0, a ; break — состояние Shift не судим + jr NZ, tr_kbd_fkclr + ld a, (__kbdraw_fakesh) + or a, a + jr NZ, tr_kbd_fkclr ; обёртка была => Shift зажат, не трогаем + ld hl, #(__kbdraw_down + _KBDRAW_LSH_BYTE) + res _KBDRAW_LSH_BIT, (hl) + ld hl, #(__kbdraw_down + _KBDRAW_RSH_BYTE) + res _KBDRAW_RSH_BIT, (hl) + tr_kbd_fkclr: + xor a, a + ld (__kbdraw_fakesh), a + ld a, e + jr tr_kbd_code ; дальше как обычная клавиша + + tr_kbd_hub: + ;; Ретрансляторы: клавиатурный блок длиннее 127 байт, а jp внутри + ;; трамплина запрещён (копия в W2, см. шапку). Стоят посередине + ;; тела — так дотягиваются и переходы сверху, и снизу. Каждый идёт + ;; после безусловного jr, провалиться сюда нельзя. + jr tr_kbd_drain + tr_hub_notkbd: + jr tr_notkbd + tr_hub_dss: + jr tr_kbd_dss + + tr_kbd_fake: + ;; E0 F0 12 / E0 12 — реальный Shift ЗАЖАТ. Бит взводит ОБЩИЙ + ;; писатель ниже: достаточно обнулить префиксы — тогда он положит + ;; код в PLAIN-половину карты (без +32) и именно взведёт бит (не + ;; break). В расширенную половину не пишем: это не клавиша. + ld a, (__kbdraw_pending) + and #1 ; 1 = break-форма (идёт ПЕРЕД обёрнутым make) + ld (__kbdraw_fakesh), a + xor a, a + ld (__kbdraw_pending), a + ld a, e + ;; fall through в tr_kbd_code ;; --- обычный байт кода: БИТ code&7 в байте _kbdraw_down[code>>3] ;; (карта упакована по битам: 64 Б вместо 512, см. _kbdraw.h). ;; Маску строим сдвигом, а НЕ таблицей: таблица потребовала бы ;; `ld hl,#метка` внутри трамплина, а он копируется в W2 побайтно ;; и обязан быть без абсолютных само-ссылок (см. шапку). --- + tr_kbd_code: ld e, a ; E = код and #7 ld b, a @@ -115,26 +182,24 @@ void _irq_tramp(void) __naked ld (hl), a xor a, a ld (__kbdraw_pending), a ; сброс накопленных префиксов - jr tr_kbd_next - - tr_kbd_setbrk: - ld hl, #__kbdraw_pending - set 0, (hl) - jr tr_kbd_next - tr_kbd_setext: - ld hl, #__kbdraw_pending - set 1, (hl) ;; fall through в tr_kbd_next tr_kbd_next: in a, (#_IRQ_SIO_A) ; ещё байт в FIFO? rrca ; бит 0 → CF - jr C, tr_kbd_drain ; да — вычерпать следующий + jr C, tr_kbd_hub ; да — вычерпать следующий (через ретранслятор) ;; --- FIFO опустошён: проверить Rx-overrun (потерян ли байт?) --- ;; RR1 SIO бит5 = Rx Overrun Error. При overrun break-код мог ;; потеряться (клавиша «залипнет») → взводим _kbdraw_overrun, чтобы ;; kbd_raw_sync() в главном цикле снял held-состояние; и Error Reset ;; (WR0 команда 0x30), иначе бит залипнет. (z84c15 SIO, Z80-совм.) + ;; + ;; СБРОС _kbdraw_pending — по образцу DSS (KBD_Receiver_Overrun в + ;; docs/sources/Estex-DSS/DSS/KEYINTER.ASM чистит KEY_FLG, свой FSM + ;; префиксов E0/F0). Без этого потерянный 0xF0/0xE0 оставляет + ;; префикс висеть, и СЛЕДУЮЩИЙ код декодируется наоборот + ;; (make↔break) или уходит в расширенную половину карты — стойкий + ;; рассинхрон, который лечится только новым нажатием (BUG-KBD-3). ld a, #1 out (#_IRQ_SIO_A), a ; указатель на RR1 in a, (#_IRQ_SIO_A) ; читаем RR1 (указатель авто-сброс в 0) @@ -142,7 +207,9 @@ void _irq_tramp(void) __naked jr Z, tr_kbd_noovr ld a, #0x30 out (#_IRQ_SIO_A), a ; WR0 Error Reset - ld a, #1 + xor a, a + ld (__kbdraw_pending), a ; префиксы недействительны + inc a ld (__kbdraw_overrun), a tr_kbd_noovr: pop bc diff --git a/libc/kbd/_kbdraw.h b/libc/kbd/_kbdraw.h index 6e8cb98..a3db341 100644 --- a/libc/kbd/_kbdraw.h +++ b/libc/kbd/_kbdraw.h @@ -23,6 +23,33 @@ extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. в extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян * байт → возможно потерян break); снимает * kbd_raw_sync (чистит _kbdraw_down) */ +/* «Fake shift» (замер на железе/MAME 2026-08-03). Пока реально зажат + * Shift, клавиатура PS/2 обёртывает КАЖДЫЙ расширенный код парой служебных + * посылок: перед make идёт E0 F0 12 (снять fake shift), после break — E0 12 + * (вернуть). Это не клавиши, а прямое и непрерывное свидетельство того, + * зажат ли Shift на самом деле, — и единственный способ узнать это, не + * полагаясь на целость потока (PS/2 состояние опросить нельзя, а typematic + * повторяет только ПОСЛЕДНЮЮ нажатую клавишу, то есть стрелку, не Shift). + * + * Декодеры (трамплин и kbd_raw_poll) используют его в обе стороны: + * - увидели fake shift -> реальный Shift ЗАЖАТ -> взвести plain-бит; + * - расширенный make БЕЗ предшествующей обёртки -> Shift ОТПУЩЕН -> + * снять plain-биты обоих Shift. + * Отсюда: залипший Shift снимается первым же нажатием стрелки, а + * kbd_raw_sync больше не обязан сбрасывать модификаторы «на всякий случай» + * (из-за чего Shift отваливался посреди игры — BUG-KBD-4). + * + * Хранит промежуточное состояние: 1 = разобрана break-форма (E0 F0 12), + * ждём расширенный код, который она обёртывает. */ +extern uint8_t _kbdraw_fakesh; + +/* Позиции бит модификаторов в карте (используют и декодеры на асме, и + * kbd_raw_sync): L Shift 0x12 -> байт 2 бит 2, R Shift 0x59 -> байт 11 бит 1. */ +#define _KBDRAW_LSH_BYTE 2 +#define _KBDRAW_LSH_BIT 2 +#define _KBDRAW_RSH_BYTE 11 +#define _KBDRAW_RSH_BIT 1 + /* Held-состояние: БИТ на скан-код, код>>3 = байт, код&7 = номер бита; * коды 0..255 обычные, 256..511 — расширенные (префикс 0xE0), т.е. * смещение +32 байта. Упаковка вместо байта на код: 64 Б вместо 512 — diff --git a/libc/kbd/_kbdraw_state.c b/libc/kbd/_kbdraw_state.c index c15a240..6a458e4 100644 --- a/libc/kbd/_kbdraw_state.c +++ b/libc/kbd/_kbdraw_state.c @@ -9,4 +9,7 @@ uint8_t _kbdraw_active; uint8_t _kbdraw_pending; uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян * break) — kbd_raw_sync снимет залипшее */ +uint8_t _kbdraw_fakesh; /* декодирована break-форма fake shift — + * ждём расширенный код, который она + * обёртывает (см. _kbdraw.h) */ uint8_t _kbdraw_down[64]; /* БИТ на скан-код (см. _kbdraw.h) */ diff --git a/libc/kbd/kbd_raw_poll.c b/libc/kbd/kbd_raw_poll.c index e2398d7..7894824 100644 --- a/libc/kbd/kbd_raw_poll.c +++ b/libc/kbd/kbd_raw_poll.c @@ -61,13 +61,54 @@ uint8_t kbd_raw_poll(void) __naked kp_drain: in a, (#_IRQ_SIO_DATA) ; чтение деструктивно = дренаж байта cp #0xF0 - jr Z, kp_setbrk + jp Z, kp_setbrk ; jp, а не jr: тело цикла длиннее 127 Б (fake shift) cp #0xE0 - jr Z, kp_setext + jp Z, kp_setext ; (в отличие от трамплина, здесь jp разрешён) + + ;; --- «fake shift» — тот же разбор, что в трамплине (см. _kbdraw.h): + ;; расширенные 12/59 не клавиши, а обёртка вокруг расширенного кода, + ;; которую клавиатура шлёт, пока реально зажат Shift. --- + ld e, a ; E = код + ld a, (__kbdraw_pending) + bit 1, a ; EXT? + ld a, e ; A = код обратно (ld флаги не трогает) + jr Z, kp_code ; обычный код — прежний путь + cp #0x12 + jr Z, kp_fake + cp #0x59 + jr Z, kp_fake + ;; расширенная КЛАВИША: make без обёртки => Shift не зажат + ld a, (__kbdraw_pending) + bit 0, a ; break — состояние Shift не судим + jr NZ, kp_fkclr + ld a, (__kbdraw_fakesh) + or a, a + jr NZ, kp_fkclr ; обёртка была => Shift зажат + ld hl, #(__kbdraw_down + _KBDRAW_LSH_BYTE) + res _KBDRAW_LSH_BIT, (hl) + ld hl, #(__kbdraw_down + _KBDRAW_RSH_BYTE) + res _KBDRAW_RSH_BIT, (hl) + kp_fkclr: + xor a, a + ld (__kbdraw_fakesh), a + ld a, e + jr kp_code + + kp_fake: + ;; Бит взводит ОБЩИЙ писатель ниже: обнуляем префиксы — и он кладёт + ;; код в PLAIN-половину карты (без +32) и именно взведёт бит. + ld a, (__kbdraw_pending) + and #1 ; 1 = break-форма (идёт ПЕРЕД обёрнутым make) + ld (__kbdraw_fakesh), a + xor a, a + ld (__kbdraw_pending), a + ld a, e + ;; fall through в kp_code ;; --- обычный байт кода: БИТ code&7 в _kbdraw_down[code>>3]. ;; Маска сдвигом, а не таблицей — ровно как в трамплине, чтобы ;; расхождения декодеров были невозможны by construction. --- + kp_code: ld e, a ; E = код and #7 ld b, a @@ -120,7 +161,7 @@ uint8_t kbd_raw_poll(void) __naked kp_next: in a, (#_IRQ_SIO_A) rrca ; ещё байт в FIFO? - jr C, kp_drain + jp C, kp_drain ;; --- FIFO пуст: тот же разбор Rx-overrun, что и в трамплине. ;; RR1 бит5 = Rx Overrun; без Error Reset (WR0 = 0x30) бит залипнет @@ -132,7 +173,11 @@ uint8_t kbd_raw_poll(void) __naked jr Z, kp_noovr ld a, #0x30 out (#_IRQ_SIO_A), a ; WR0 Error Reset - ld a, #1 + ;; Сброс FSM префиксов — как KBD_Receiver_Overrun у DSS (чистит + ;; KEY_FLG): повисший 0xF0/0xE0 инвертировал бы следующий код. + xor a, a + ld (__kbdraw_pending), a + inc a ld (__kbdraw_overrun), a ; снимет kbd_raw_sync() kp_noovr: pop bc diff --git a/libc/kbd/kbd_raw_sync.c b/libc/kbd/kbd_raw_sync.c index 2b25345..4d5dcf4 100644 --- a/libc/kbd/kbd_raw_sync.c +++ b/libc/kbd/kbd_raw_sync.c @@ -3,21 +3,40 @@ * * Аппаратный приёмный FIFO SIO — 3 байта. Если прерывания запрещены * достаточно долго (DI-окна графики/кадрового IRQ), а в этот момент - * приходит пачка скан-кодов (напр. быстрый тап стрелки = E0 74 + E0 F0 74, - * 5 байт) — FIFO переполняется, байт(ы) теряются аппаратно. Потерянный - * break → клавиша «залипает» в _kbdraw_down. + * приходит пачка скан-кодов — FIFO переполняется, байт(ы) теряются + * аппаратно. Потерянный break → клавиша «залипает» в _kbdraw_down. * - * Трамплин ловит overrun (RR1 бит5) и взводит _kbdraw_overrun. Приложение - * зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывается - * held-состояние ВСЕХ клавиш, КРОМЕ модификаторов (Shift/Ctrl/Alt — - * таблица ниже). Обычные реально зажатые клавиши «перечитаются» их - * make-повтором (typematic) за ~0.1 с. Модификаторы НЕ сбрасываем - * принципиально: PS/2 автоповторяет только ПОСЛЕДНЮЮ нажатую клавишу, - * удерживаемый Shift при повторе стрелки не шлёт НИЧЕГО — сброшенный - * модификатор невозможно перечитать, он «отваливался» до перенажатия - * (баг PoP: Shift+→ = осторожный шаг превращался в бег, 2026-07-22). - * Цена: если overrun потерял именно break модификатора, тот залипнет до - * следующего нажатия — редкий случай против регулярного отвала. + * Трамплин (и kbd_raw_poll) ловят overrun (RR1 бит5): делают Error Reset, + * чистят FSM префиксов _kbdraw_pending и взводят _kbdraw_overrun. + * Приложение зовёт kbd_raw_sync() раз в кадр — по флагу сбрасывается + * held-состояние карты. Реально зажатые клавиши перечитаются make-повтором + * (typematic) за ~0.1 с. + * + * МОДИФИКАТОРЫ ИЗ СБРОСА ИСКЛЮЧЕНЫ (2026-08-03, третья редакция). История + * вопроса — две неудачные крайности: + * + * 1) исключать модификаторы, не имея иного способа их проверить: + * потерянный break Shift снять нечем, Shift залипает навсегда + * (BUG-KBD-3); + * 2) сбрасывать ВСЁ, как DSS (KBD_Receiver_Overrun в KEYINTER.ASM чистит + * и KEYCTRL, и KEY_FLG): залипания нет, но удерживаемый Shift сносится + * каждым overrun'ом, а при зажатом Shift КАЖДЫЙ тап стрелки — это 10 + * байт в 3-байтовый FIFO, то есть overrun почти гарантирован. Игрок + * жмёт Shift+→ ожидая осторожный шаг, а Кид уходит в бег — в PoP это + * прямо стоит жизни (яма/пики). Хуже залипания (BUG-KBD-4). + * + * Третий вариант убирает сам размен. Те самые лишние байты — это «fake + * shift» (E0 F0 12 перед расширенным make, E0 12 после break), и шлются они + * ТОЛЬКО пока Shift реально зажат. Декодеры (_irq_tramp.c, kbd_raw_poll.c) + * теперь читают их в обе стороны: обёртка есть → Shift взвести, расширенный + * make без обёртки → Shift снять. Значит: + * - сбрасывать модификаторы здесь НЕ НАДО (состояние Shift подтверждается + * независимо от того, что съел overrun); + * - залипание невозможно: первый же тап стрелки снимет ложный Shift. + * + * Ctrl/Alt fake-shift'ом не покрыты (клавиатура их не обёртывает). Их + * оставляем по тому же принципу «лучше залипание, чем отвал»: залипший + * Ctrl/Alt в PoP не управляет ничем критичным и снимается перенажатием. */ #include #include "_kbdraw.h" @@ -47,4 +66,10 @@ void kbd_raw_sync(void) uint16_t c = _kbd_mod_codes[i]; if (save[i]) _kbdraw_down[c >> 3] |= (uint8_t)(1 << (c & 7)); } + /* FSM префиксов трамплин уже сбросил; дублируем на случай overrun'а, + * пойманного вне его (kbd_raw_poll между кадрами). Незакрытую обёртку + * fake shift тоже считаем недействительной: её вторая половина могла + * пропасть вместе с потерянными байтами. */ + _kbdraw_pending = 0; + _kbdraw_fakesh = 0; }