6dabe9b4b1
Корень двух багов SprPoP (KBD-STUCK-WAIT, частично GRAB-KBD-TIMING) нашёлся в исходниках DSS (docs/sources/Estex-DSS): обработчик прерывания DSS живёт в IM1 по 0x0038 и ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`, а тот вычерпывает FIFO SIO досуха. Наш трамплин проверял «есть ли клавиатурный байт» один раз, на входе в прерывание, а хвост кадрового пути уходил в DSS — значит скан-код, прилетевший позже, доставался DSS и уезжал в его буфер. Rx-overrun при этом НЕ взводится (байт не потерян железом, а прочитан не тем владельцем) — отсюда и загадка исходного диагноза: бит залип при `_kbdraw_overrun == 0`. Измерено в MAME (брейки + totalcycles): окно 738 тактов (~34 мкс) на каждом кадровом прерывании, из них 481 такт — пролог самого DSS. Поэтому проверка FIFO перед chain'ом снимает лишь треть и не годится (пробовали, кражи продолжались); кадровый путь при открытом raw теперь заканчивается приватным RETI, окно = 0. Цена: на это время у DSS замирает опрос мыши и мигание текстового курсора — зафиксировано в <kbd_raw.h>. Пойманный случай (старая сборка): DSS прочитал 0x74 (make стрелки «вправо») при _kbdraw_pending = EXT, то есть посылку E0 74 разорвало пополам между двумя владельцами канала. Проверка: брейк на входе KEYSCAN с условием «страница точно DSS + raw открыт» до правки срабатывал мгновенно (50/с), после — молчит; положительный контроль на нашем RETI срабатывает сразу. Трамплин 300 -> 310 Б (буфер W2-копии поднят 336 -> 384, запас 74 Б); размерный эталон обновлён: +10 Б у программ, линкующих IRQ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
359 lines
18 KiB
C
359 lines
18 KiB
C
/*
|
||
* _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL),
|
||
* версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes.
|
||
*
|
||
* Разбор источника (трогаем только AF, пока не выяснили):
|
||
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → штатно сразу к DSS,
|
||
* БЕЗ remap (W1 не трогаем — прерванный контекст сохранён как
|
||
* есть). ИСКЛЮЧЕНИЕ: если raw-канал открыт (`_kbdraw_active`,
|
||
* см. libc/kbd/_kbdraw.h, kbd_raw_open) — байт достаётся НАМ
|
||
* (порт данных 0x18 читается один раз, деструктивно — либо мы,
|
||
* либо DSS, не оба), декодируется make/break (PS/2 Scan Code
|
||
* Set 2: 0xF0 — префикс отпускания, 0xE0 — префикс расширенной
|
||
* клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
|
||
* и к DSS НЕ идём
|
||
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
|
||
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>) — и на
|
||
* кадровом пути тоже: пока raw открыт, обработчик DSS НЕ вызывается
|
||
* совсем (иначе его KEYSCAN крадёт наши байты — см. tr_frame_exit).
|
||
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
|
||
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
|
||
* без чейна к DSS;
|
||
* - кадровое: проход по _irq_chain[] — все ненулевые слоты по порядку.
|
||
*
|
||
* W1-remap (docs/im2_isr_design.md «Вариант работает во всех режимах»):
|
||
* в small/huge базовый код приложения (и хендлеры) в W1, а сам трамплин
|
||
* во время прерывания исполняется из W2-КОПИИ (_irq_table_ref копирует
|
||
* этот код в _irq_tramp_w2buf, т.к. оригинал в W1 может быть перемаплен
|
||
* DSS). Перед вызовом хендлеров копия сохраняет текущую W1-страницу и
|
||
* ставит базовую страницу приложения (_irq_app_w1_page), после —
|
||
* возвращает сохранённую (перед chain к DSS / перед RETI). Единый
|
||
* «restore W1=base» покрывает хендлер в W1, в W2 и на стыке 0x8000.
|
||
* В tiny/big remap безвреден (хендлеры в W2, W1 всё равно восстановлен).
|
||
*
|
||
* COPY-SAFE: все внутренние переходы — jr/djnz (относительные);
|
||
* единственный call — к _irq_call_hl (исполняется при W1=base, значит
|
||
* оригинал в W1 достижим); оба jp — литерал 0x0038 (W0, всегда
|
||
* замаплен). Никаких абсолютных само-ссылок → байты копируются как
|
||
* есть, без релокации. ВНИМАНИЕ: не добавлять `jp <метка>` внутрь
|
||
* трамплина — сломает копию.
|
||
*/
|
||
|
||
#include "_irq.h"
|
||
#include "../kbd/_kbdraw.h"
|
||
|
||
void _irq_tramp(void) __naked
|
||
{
|
||
__asm
|
||
push af ; сохранить прерванный AF
|
||
in a, (#_IRQ_SIO_A)
|
||
rrca ; бит 0 — клавиатурный байт принят?
|
||
jr NC, tr_hub_notkbd ; не клавиатура (через ретранслятор — см. ниже)
|
||
|
||
;; --- клавиатура ---
|
||
ld a, (__kbdraw_active)
|
||
or a, a
|
||
jr Z, tr_hub_dss ; raw выключен — прежнее поведение
|
||
|
||
;; --- raw включён: байт наш целиком, DSS его не увидит ---
|
||
;; ВЫЧЕРПЫВАЕМ FIFO SIO В ЦИКЛЕ (как эталон keybIntHandler /
|
||
;; PS2_Scan в docs/samples): приёмный FIFO Z80 SIO 3 байта
|
||
;; глубиной, а за один кадр при одновременном отпускании
|
||
;; нескольких клавиш приходит ПАЧКА break-кодов (0xF0 code ...).
|
||
;; Читать один байт за прерывание = переполнение FIFO = потеря
|
||
;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16).
|
||
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 NZ, tr_kbd_nF0
|
||
ld hl, #__kbdraw_pending
|
||
set 0, (hl)
|
||
jr tr_kbd_hub
|
||
tr_kbd_nF0:
|
||
cp #0xE0
|
||
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
|
||
;; расширенная КЛАВИША — обычный путь. О состоянии Shift по ней НЕ
|
||
;; судим (почему — см. tr_kbd_fake). A всё ещё = код.
|
||
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-формы.
|
||
;; В расширенную половину не пишем: это не клавиша.
|
||
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
|
||
inc b ; B = (код&7)+1 для djnz
|
||
ld a, #1
|
||
jr tr_kbd_mskdec
|
||
tr_kbd_mskloop:
|
||
add a, a
|
||
tr_kbd_mskdec:
|
||
djnz tr_kbd_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, tr_kbd_noext
|
||
ld de, #32 ; +256 кодов = +32 байта карты
|
||
add hl, de
|
||
tr_kbd_noext:
|
||
ld a, (__kbdraw_pending)
|
||
bit 0, a ; _KBDRAW_PEND_BREAK?
|
||
jr Z, tr_kbd_mkdown
|
||
ld a, c
|
||
cpl ; break → сбросить бит
|
||
and a, (hl)
|
||
jr tr_kbd_wr
|
||
tr_kbd_mkdown:
|
||
ld a, c ; make → взвести бит
|
||
or a, (hl)
|
||
tr_kbd_wr:
|
||
ld (hl), a
|
||
xor a, a
|
||
ld (__kbdraw_pending), a ; сброс накопленных префиксов
|
||
;; fall through в tr_kbd_next
|
||
tr_kbd_next:
|
||
in a, (#_IRQ_SIO_A) ; ещё байт в FIFO?
|
||
rrca ; бит 0 → CF
|
||
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)
|
||
bit 5, a
|
||
jr Z, tr_kbd_noovr
|
||
ld a, #0x30
|
||
out (#_IRQ_SIO_A), a ; WR0 Error Reset
|
||
xor a, a
|
||
ld (__kbdraw_pending), a ; префиксы недействительны
|
||
inc a
|
||
ld (__kbdraw_overrun), a
|
||
tr_kbd_noovr:
|
||
pop bc
|
||
pop de
|
||
pop hl
|
||
pop af
|
||
ei
|
||
reti ; raw владеет каналом целиком — к DSS НЕ идём
|
||
|
||
tr_kbd_dss:
|
||
;; --- прежнее поведение: прямо к DSS, без remap ---
|
||
pop af
|
||
.db 0xC3 ; jp nn
|
||
__irq_dss_target::
|
||
.dw 0x0038 ; SMC-операбельно, но по умолчанию 0x0038
|
||
|
||
tr_notkbd:
|
||
;; --- CBL? --- старший байт хука ненулевой у любого W2-адреса.
|
||
ld a, (__irq_cbl_hook + 1)
|
||
or a, a
|
||
jr Z, tr_frame
|
||
in a, (#0xFE)
|
||
rlca ; бит 7 — запрос блока CBL?
|
||
jr NC, tr_frame
|
||
|
||
;; --- CBL: remap-скобка + хук + приватный RETI ---
|
||
in a, (#_IRQ_W1_PORT) ; сохранить прерванную W1
|
||
ld (__irq_saved_w1), a
|
||
ld a, (__irq_app_w1_page)
|
||
out (#_IRQ_W1_PORT), a ; W1 = базовая страница приложения
|
||
push bc
|
||
push de
|
||
push hl
|
||
.db 0x08 ; ex af,af (теневой AF)
|
||
push af
|
||
exx
|
||
push bc
|
||
push de
|
||
push hl
|
||
push ix
|
||
push iy
|
||
ld hl, (__irq_cbl_hook)
|
||
call _irq_call_hl
|
||
pop iy
|
||
pop ix
|
||
pop hl
|
||
pop de
|
||
pop bc
|
||
exx
|
||
pop af
|
||
.db 0x08 ; ex af,af
|
||
pop hl
|
||
pop de
|
||
pop bc
|
||
ld a, (__irq_saved_w1) ; вернуть прерванную W1
|
||
out (#_IRQ_W1_PORT), a
|
||
pop af
|
||
ei
|
||
reti
|
||
|
||
tr_frame:
|
||
;; --- кадровое: цепочка обработчиков ---
|
||
ld a, (__irq_chain_n)
|
||
or a, a
|
||
jr Z, tr_frame_exit ; нет хендлеров → к DSS без remap/сейва
|
||
|
||
in a, (#_IRQ_W1_PORT) ; remap-скобка: сохранить + база
|
||
ld (__irq_saved_w1), a
|
||
ld a, (__irq_app_w1_page)
|
||
out (#_IRQ_W1_PORT), a
|
||
;; полный сейв обоих наборов + индексные (хендлеры клобберят всё)
|
||
push bc
|
||
push de
|
||
push hl
|
||
.db 0x08
|
||
push af
|
||
exx
|
||
push bc
|
||
push de
|
||
push hl
|
||
push ix
|
||
push iy
|
||
|
||
;; проход по слотам: HL идёт по _irq_chain, B = счётчик слотов
|
||
ld hl, #__irq_chain
|
||
ld b, #IRQ_CHAIN_MAX
|
||
tr_loop:
|
||
ld e, (hl)
|
||
inc hl
|
||
ld d, (hl)
|
||
inc hl
|
||
ld a, d
|
||
or a, e
|
||
jr Z, tr_skip ; пустой слот
|
||
push bc ; сохранить счётчик + указатель цепи
|
||
push hl
|
||
ex de, hl ; HL = адрес хендлера
|
||
call _irq_call_hl
|
||
pop hl
|
||
pop bc
|
||
tr_skip:
|
||
djnz tr_loop
|
||
|
||
pop iy
|
||
pop ix
|
||
pop hl
|
||
pop de
|
||
pop bc
|
||
exx
|
||
pop af
|
||
.db 0x08
|
||
pop hl
|
||
pop de
|
||
pop bc
|
||
ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain
|
||
out (#_IRQ_W1_PORT), a
|
||
tr_frame_exit:
|
||
;; --- ПОКА RAW ОТКРЫТ, В DSS НЕ ХОДИМ ВООБЩЕ ---
|
||
;; Хвост кадрового пути уходил в DSS (0x0038), а его обработчик
|
||
;; INTx38_Handler ПЕРВЫМ ДЕЛОМ зовёт KEYSCAN, и тот вычерпывает FIFO
|
||
;; SIO досуха (docs/sources/Estex-DSS/DSS/DSS-MAIN.ASM,
|
||
;; KEYINTER.ASM:KEYSCAN.RESCAN). Наша проверка «есть ли байт» стоит в
|
||
;; НАЧАЛЕ трамплина, поэтому скан-код, прилетевший позже, доставался
|
||
;; DSS и уезжал в ЕГО буфер — БЕЗ Rx-overrun. Украденный make =
|
||
;; несработавшее нажатие, украденный break = залипшая навсегда
|
||
;; клавиша при `_kbdraw_overrun == 0` (BUGS_OPEN KBD-STUCK-WAIT,
|
||
;; GRAB-KBD-TIMING). Поймано в MAME 2026-08-28: DSS прочитал 0x74
|
||
;; (make стрелки «вправо») при `_kbdraw_pending = EXT` — посылка
|
||
;; `E0 74` разорвана пополам между двумя владельцами канала.
|
||
;;
|
||
;; Окно измерено брейками + totalcycles: от входа трамплина до
|
||
;; чтения DSS — 738 тактов (~34 мкс), и открыто оно на КАЖДОМ
|
||
;; кадровом прерывании (50/с). Из них 481 такт — пролог самого DSS
|
||
;; (jp 0x38 → jp .Handler → call INTx38_Handler → 11 push → call
|
||
;; KEYSCAN), то есть закрыть окно проверкой FIFO перед chain'ом
|
||
;; нельзя: она снимает лишь треть (проверено там же).
|
||
;;
|
||
;; Поэтому пока raw открыт, кадровый путь заканчивается ПРИВАТНЫМ
|
||
;; RETI и обработчик DSS не вызывается совсем — окно ровно ноль.
|
||
;; ЦЕНА: на это время замирает кадровое обслуживание DSS —
|
||
;; Dss.Mouse.GetPackets и мигание текстового курсора. Приложению с
|
||
;; raw-клавиатурой (игре) не нужно ни то, ни другое; кому нужна мышь
|
||
;; DSS одновременно с raw — не открывать raw (см. <kbd_raw.h>).
|
||
ld a, (__kbdraw_active)
|
||
or a, a
|
||
jr Z, tr_chain_dss
|
||
pop af
|
||
ei
|
||
reti
|
||
tr_chain_dss:
|
||
pop af
|
||
.db 0xC3 ; jp 0x0038 (литерал — copy-safe)
|
||
.dw 0x0038
|
||
|
||
_irq_call_hl:
|
||
jp (hl)
|
||
__irq_tramp_end::
|
||
__endasm;
|
||
}
|