libc+libbgi: raw-клавиатура (held-state) + column-major блит спрайтов

libc/kbd: kbd_raw_open/close/down — сырой PS/2-канал клавиатуры с
held-state (битовая карта _kbdraw_down[512], EXT-клавиши +256).  Пока
raw открыт, кадровый IRQ-трамплин перехватывает байт SIO у DSS и
декодирует make/break (0xF0/0xE0-префиксы) сам.  FIFO вычерпывается В
ЦИКЛЕ (приёмный буфер SIO 3 байта; пачка break-кодов при одновременном
отпускании иначе теряется → залипание клавиши).  Буфер W2-трамплина
поднят 224→288 Б под выросший обработчик.

libc/conio: kbd_mod_state() — live-состояние модификаторов (ESTEX
CTRLKEY $33h), Shift/Ctrl/Alt/Lock прямо сейчас, KBD_MOD_* маска.

libbgi: gfx_blit_cols(x,y,img,flip) + _bgi_blit_cols_raw — блит
column-major спрайта вертикальным accel-проходом, бесплатный
горизонтальный флип (sstride<0), клип по экрану.  Для персонажей.

libbgi/atlas_load: восстанавливать W3 ДО записи a->count (atlas_t в
--bank памяти резолвится через W3; count оставался мусором).

tests/kbdraw — тест raw-клавиатуры; size-baseline +kbdraw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:48:32 +03:00
parent e14b6745f7
commit 484b18d10c
19 changed files with 602 additions and 7 deletions
+26
View File
@@ -0,0 +1,26 @@
/*
* kbd_mod_state — обёртка ESTEX CTRLKEY ($33h): состояние
* модификаторов клавиатуры НА ДАННЫЙ МОМЕНТ (не из буфера — драйвер
* берёт данные прямо из последнего скана, см. ProgrammerManual.txt
* "CTRLKEY"), поэтому в отличие от kbhit/getch/getkey держится ровно
* столько, сколько клавиша реально зажата.
*
* ABI: C=0x33 на входе — функция; на выходе C=mode, B=shift (тот же
* регистр C переиспользуется под результат, как и во всех ESTEX-
* функциях клавиатуры). Расшифровка битов — KBD_MOD_* в <conio.h>.
*/
#include "_conio.h"
uint16_t kbd_mod_state(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY
rst #0x10
pop ix
ld d, c ; D = mode (старший байт результата)
ld e, b ; E = shift (младший байт результата)
ret ; __sdcccall(1): DE = uint16_t
__endasm;
}
+23
View File
@@ -75,6 +75,29 @@ uint16_t getkey(void);
#define KEY_INS 0x50 /* numpad 0; not verified */
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
/* kbd_mod_state — live-состояние модификаторов ПРЯМО СЕЙЧАС (ESTEX
* CTRLKEY $33h), а не событие из буфера: держится столько, сколько
* клавиша реально зажата (в отличие от kbhit/getch/getkey). Покрывает
* ТОЛЬКО Shift/Ctrl/Alt/Rus-Lat/Lock-клавиши — обычные клавиши
* (стрелки и т.п.) у ESTEX live-state не имеют, см. <kbd_raw.h>.
*
* Возврат: (mode << 8) | shift — расшифровка через KBD_MOD_* ниже. */
uint16_t kbd_mod_state(void);
#define KBD_MOD_RUSLAT 0x8000
#define KBD_MOD_NUMLOCK 0x0800
#define KBD_MOD_SCRLOCK 0x0400
#define KBD_MOD_INSERT 0x0200
#define KBD_MOD_CAPSLOCK 0x0100
#define KBD_MOD_LSHIFT 0x0080
#define KBD_MOD_RSHIFT 0x0040
#define KBD_MOD_CTRL 0x0020
#define KBD_MOD_ALT 0x0010
#define KBD_MOD_LCTRL 0x0008
#define KBD_MOD_LALT 0x0004
#define KBD_MOD_RCTRL 0x0002
#define KBD_MOD_RALT 0x0001
typedef union {
uint16_t value;
struct {
+63
View File
@@ -0,0 +1,63 @@
/*
* 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);
#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
#endif
+8 -2
View File
@@ -30,8 +30,10 @@
#define _IRQ_VEC_BUF_SIZE (256 + 257)
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
* W1). С запасом над реальным размером трамплина (~150 Б). */
#define _IRQ_TRAMP_BUF_SIZE 224
* W1). С запасом над реальным размером трамплина (225 Б после
* FIFO-drain цикла в raw-клавиатурной ветке, 2026-07-16 — проверять
* __irq_tramp_end при правках, запас держать ≥ 60 Б). */
#define _IRQ_TRAMP_BUF_SIZE 288
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
@@ -63,6 +65,10 @@ extern isr_t _irq_cbl_hook;
#define _IRQ_CTC_CH2 0x12
#define _IRQ_CTC_CH3 0x13
#define _IRQ_SIO_A 0x19 /* SIO-A RR0: бит 0 = Rx available */
#define _IRQ_SIO_DATA 0x18 /* SIO-A data — деструктивное чтение
* (дренирует байт); читает либо DSS
* (штатно), либо raw-канал клавиатуры
* (libc/kbd/_kbdraw.h), но не оба сразу */
/* Захватить/отпустить IM2-таблицу (счётчик ссылок). ref: при первом
* захвате строит таблицу, ставит I/IM2, взводит atexit-уборку;
+76 -3
View File
@@ -3,8 +3,16 @@
* версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes.
*
* Разбор источника (трогаем только AF, пока не выяснили):
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → сразу к DSS, БЕЗ
* remap (W1 не трогаем — прерванный контекст сохранён как есть);
* - клавиатура (бит 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[]`, и к DSS НЕ идём
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
* без чейна к DSS;
@@ -29,6 +37,7 @@
*/
#include "_irq.h"
#include "../kbd/_kbdraw.h"
void _irq_tramp(void) __naked
{
@@ -38,7 +47,71 @@ void _irq_tramp(void) __naked
rrca ; бит 0 клавиатурный байт принят?
jr NC, tr_notkbd
;; --- клавиатура: прямо к DSS, без remap ---
;; --- клавиатура ---
ld a, (__kbdraw_active)
or a, a
jr Z, tr_kbd_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
tr_kbd_drain:
in a, (#_IRQ_SIO_DATA) ; читаем тем самым дренируем байт
cp #0xF0
jr Z, tr_kbd_setbrk
cp #0xE0
jr Z, tr_kbd_setext
;; --- обычный байт кода: индекс в _kbdraw_down[код(+256 EXT)] ---
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, #256
add hl, de
tr_kbd_noext:
ld a, (__kbdraw_pending)
bit 0, a ; _KBDRAW_PEND_BREAK?
jr Z, tr_kbd_mkdown
xor a, a ; break снять (0)
jr tr_kbd_wr
tr_kbd_mkdown:
ld a, #1 ; make взвести (1)
tr_kbd_wr:
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 ; да вычерпать следующий
pop de
pop hl
pop af
ei
reti ; raw владеет каналом целиком к DSS НЕ идём
tr_kbd_dss:
;; --- прежнее поведение: прямо к DSS, без remap ---
pop af
.db 0xC3 ; jp nn
__irq_dss_target::
+26
View File
@@ -0,0 +1,26 @@
/*
* _kbdraw.h — внутренности raw-канала клавиатуры (НЕ публичный
* заголовок; публичный интерфейс — <kbd_raw.h>).
*
* Всё состояние — плоские глобалы (живут в W2/_DATA во ВСЕХ memory-
* режимах, см. sprinter_memory_modes) — трамплин прерывания
* (libc/irq/_irq_tramp.c) обращается к ним БЕЗ W1-remap и без call,
* поэтому раскладка памяти приложения тут ни при чём.
*/
#ifndef _KBDRAW_INTERNAL_H
#define _KBDRAW_INTERNAL_H
#include <stdint.h>
/* Биты _kbdraw_pending — накопленные префиксы PS/2 Scan Code Set 2,
* ждущие завершающего байта кода (см. _irq_tramp.c). */
#define _KBDRAW_PEND_BREAK 0x01
#define _KBDRAW_PEND_EXT 0x02
extern uint8_t _kbdraw_active; /* 0 = raw выключен (дефолт, трамплин
* ведёт себя как раньше — прямо к DSS) */
extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. выше */
extern uint8_t _kbdraw_down[512]; /* 0/1 по коду; +256 для расширенных
* (префикс 0xE0) — см. kbd_raw_down */
#endif
+10
View File
@@ -0,0 +1,10 @@
/*
* _kbdraw_state.c — состояние raw-канала клавиатуры (см. _kbdraw.h).
* Не инициализировать явно = 0 — crt0 зануляет _DATA/_BSS
* (memory/sdcc_static_storage_gotcha).
*/
#include "_kbdraw.h"
uint8_t _kbdraw_active;
uint8_t _kbdraw_pending;
uint8_t _kbdraw_down[512];
+17
View File
@@ -0,0 +1,17 @@
/*
* kbd_raw_close — выключить raw-канал, вернуть клавиатуру DSS
* (kbhit/getch/getkey/kbd_mod_state снова получают события).
* Идемпотентно; висит на atexit (kbd_raw_open).
*/
#include "_kbdraw.h"
#include "../irq/_irq.h"
void kbd_raw_close(void)
{
if (!_kbdraw_active) return;
IRQ_DISABLE();
_kbdraw_active = 0;
IRQ_ENABLE();
_irq_table_unref();
}
+14
View File
@@ -0,0 +1,14 @@
/*
* kbd_raw_down — зажата ли клавиша code ПРЯМО СЕЙЧАС (0/1).
* Читает битовую карту, которую ведёт трамплин прерывания
* (libc/irq/_irq_tramp.c) — O(1), никаких ESTEX-вызовов.
*/
#include <kbd_raw.h>
#include "_kbdraw.h"
uint8_t kbd_raw_down(uint16_t code)
{
if (code >= 512) return 0;
return _kbdraw_down[code];
}
+40
View File
@@ -0,0 +1,40 @@
/*
* kbd_raw_open — включить эксклюзивный raw-канал клавиатуры: с этого
* момента байты SIO-A клавиатуры декодирует трамплин ПРЯМО в
* прерывании (_kbdraw_down[], см. libc/irq/_irq_tramp.c), DSS их
* больше не видит — kbhit/getch/getkey/kbd_mod_state перестают
* получать новые события (applications/PoP/docs/PORT_PLAN.md §2).
*
* Захватывает IM2-таблицу тем же счётчиком ссылок, что
* irq_chain_add/cbl_open (_irq_table_ref) — можно сочетать с кадровой
* цепочкой и CBL одновременно, они независимы (разные условия ветвления
* в трамплине). atexit подстрахует close.
*/
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sprinter_exit.h>
#include <kbd_raw.h>
#include "_kbdraw.h"
#include "../irq/_irq.h"
int kbd_raw_open(void)
{
if (_kbdraw_active) { errno = EBUSY; return -1; }
if (_irq_table_ref() != 0) return -1;
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
_kbdraw_pending = 0;
static uint8_t atexit_armed;
if (!atexit_armed) {
atexit_armed = 1;
atexit(kbd_raw_close);
}
IRQ_DISABLE();
_kbdraw_active = 1;
IRQ_ENABLE();
return 0;
}