Files
Sprinter-SDCC/libc/irq/_irq_tramp.c
T
snark13 6dabe9b4b1 libc: пока raw-клавиатура открыта, не звать обработчик DSS — он крал наши скан-коды
Корень двух багов 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>
2026-08-28 11:40:39 +03:00

359 lines
18 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.
/*
* _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 оставляет
;; префикс висеть, и СЛЕДУЮЩИЙ код декодируется наоборот
;; (makebreak) или уходит в расширенную половину карты стойкий
;; рассинхрон, который лечится только новым нажатием (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;
}