From 484b18d10c80d318586a035584af5f96222b1f54 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Fri, 17 Jul 2026 17:48:32 +0300 Subject: [PATCH] =?UTF-8?q?libc+libbgi:=20raw-=D0=BA=D0=BB=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=B0=D1=82=D1=83=D1=80=D0=B0=20(held-state)=20+=20colum?= =?UTF-8?q?n-major=20=D0=B1=D0=BB=D0=B8=D1=82=20=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B9=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/libc-reference.md | 56 ++++++++++++++++++++ docs/size_baseline.tsv | 1 + libbgi/_bgi.h | 9 ++++ libbgi/bgi256/_bgi_blit_cols_raw.c | 84 ++++++++++++++++++++++++++++++ libbgi/common/atlas_load.c | 13 ++++- libbgi/common/gfx_blit_cols.c | 61 ++++++++++++++++++++++ libbgi/include/gfx.h | 7 +++ libc/conio/kbd_mod_state.c | 26 +++++++++ libc/include/conio.h | 23 ++++++++ libc/include/kbd_raw.h | 63 ++++++++++++++++++++++ libc/irq/_irq.h | 10 +++- libc/irq/_irq_tramp.c | 79 ++++++++++++++++++++++++++-- libc/kbd/_kbdraw.h | 26 +++++++++ libc/kbd/_kbdraw_state.c | 10 ++++ libc/kbd/kbd_raw_close.c | 17 ++++++ libc/kbd/kbd_raw_down.c | 14 +++++ libc/kbd/kbd_raw_open.c | 40 ++++++++++++++ tests/kbdraw/Makefile | 5 ++ tests/kbdraw/kbdraw.c | 65 +++++++++++++++++++++++ 19 files changed, 602 insertions(+), 7 deletions(-) create mode 100644 libbgi/bgi256/_bgi_blit_cols_raw.c create mode 100644 libbgi/common/gfx_blit_cols.c create mode 100644 libc/conio/kbd_mod_state.c create mode 100644 libc/include/kbd_raw.h create mode 100644 libc/kbd/_kbdraw.h create mode 100644 libc/kbd/_kbdraw_state.c create mode 100644 libc/kbd/kbd_raw_close.c create mode 100644 libc/kbd/kbd_raw_down.c create mode 100644 libc/kbd/kbd_raw_open.c create mode 100644 tests/kbdraw/Makefile create mode 100644 tests/kbdraw/kbdraw.c diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 4821d60..261203a 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -163,6 +163,11 @@ libc отказывает сама (EMFILE, предохранитель _fd_gua Клавиатура: `kbhit getch getche getkey` (+KEY_* коды позиций), `char *cgets(buf)`. +`uint16_t kbd_mod_state(void)` — live-состояние модификаторов ПРЯМО +СЕЙЧАС (ESTEX CTRLKEY $33h, не событие из буфера — держится, пока +клавиша реально зажата); (mode<<8)|shift, расшифровка KBD_MOD_*. +Покрывает только Shift/Ctrl/Alt/Rus-Lat/Lock — для обычных клавиш +(стрелки и т.п.) live-state у ESTEX нет, см. ``. Вывод с атрибутом: `putch cputs cprintf` (~10× медленнее stdio-пути; '\n' НЕ транслируется — писать "\r\n"). Атрибуты: `textcolor textbackground textattr`, `set/get_text_attr`, @@ -285,6 +290,57 @@ Cross-lib: libbgi всегда линкуется с libc; _cbl_port_ref/unref Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор, banked-функции; только volatile-глобалы и быстрая работа (<1 мс). +## — эксклюзивный raw-канал клавиатуры (вектор 0xFF, свой путь) + +Мотивация — applications/PoP/docs/PORT_PLAN.md §2: ESTEX +kbhit/getch/getkey/kbd_mod_state — событийные, без held-state для +обычных (не модификаторных) клавиш. Decode make/break (PS/2 Scan Code +Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс расширенной +клавиши) прямо в трамплине прерывания (libc/irq/_irq_tramp.c), по +прецеденту приватного пути CBL (не чейнится к DSS, пока открыт). + +| Сигнатура | Описание | +|---|---| +| `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) | +| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit | +| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 | +| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) | +| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно | + +**ИСПРАВЛЕНО 2026-07-15 (был неверный вывод, ниже — то, что реально +подтвердилось):** изначально скриншот-тестирование (снимок экрана +после `press_key('up')`) не показывало видимого эффекта на +UP/DOWN/RIGHT — из этого сделан ОШИБОЧНЫЙ вывод «скрипт не может их +нажать». На самом деле причина была в неудачном таймінге снимков +относительно дуги прыжка, а не в отсутствии сигнала. Подтверждено +брейкпоинтом в отладчике MAME (adress уровня приложения на входе в +код `jumping=1` в `applications/PoP/poc/poc.c`): `press_key('up')` +ЧЕСТНО доходит до raw-декодера и триггерит код прыжка — брейкпоинт +сработал ровно один раз за одно удержание клавиши (это же заодно +подтвердило фикс двойного триггера через `up_prev` edge-detect в +`poc.c`). Так что скриптовый `press_key` для UP/DOWN/RIGHT РАБОТАЕТ +корректно так же, как для LEFT — предыдущая запись про «ограничение +именно в скриптовом инжекте» была неверной, оставлена в истории +git как урок: при повторных «нет эффекта» на скриншотах — проверять +брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться +только на визуальный снимок с произвольным таймингом. + +**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/ +kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт +то же самое, что было на момент открытия — резидентный обработчик DSS, +от которого зависит его live-state, во время raw не выполняется). +ESC для выхода — через `kbd_raw_down(KBD_ESC)`, не через DSS. + +Верификация (tests/kbdraw, MAME, 2026-07-15): `press_key` в мосте +MAME дёргает ОБЕ клавиатуры (PC ms_naturl + ZX-матрица `IO_LINE`) +одновременно — наблюдался паразитный незатухающий бит от ZX-пути +(обычный код без EXT-префикса, застревал в DOWN) — не воспроизводится +на реальном сценарии (только PC/AT-клавиатура, без матрицы, см. +applications/PoP/docs/PORT_PLAN.md §2 — пользователь подтвердил, что +матрица на Sprinter давно не используется). Для будущих MAME-тестов +этой функции — бить только по `:kbd:ms_naturl:*`, не через +удобный `press_key` (или перепроверить точную семантику полей моста). + ## — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR) Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 893b964..dfcdc86 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -33,6 +33,7 @@ gfxbanks 6044 hello 4167 hello2 4244 irqtest 6527 +kbdraw 4911 ls 4835 malloc 4461 mdview 18878 diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 8ce80f2..dddcdb4 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -86,6 +86,15 @@ void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst, uint8_t w, uint8_t h, int sstride, uint8_t y0); +/* КОЛОНОЧНАЯ специализация (column-major src -> экран): каждая колонка — + * вертикальный accel-burst (LD A,A, Port_Y авто-шаг); dst +1/колонку, src + * +sstride/колонку. Даёт бесплатный горизонтальный флип (sstride<0 + src + * с конца). w=колонок, h=высота(=размер блока), y0=Port_Y стартовый. + * bgi256/_bgi_blit_cols_raw.c. */ +void _bgi_blit_cols_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0); + /* Специализация для heal: src == dst == адрес экрана, оба страйда 0 * (строку выбирает Port_Y) — адресной арифметики в цикле нет, строка * 77Т + burst'ы. y0 — стартовый Port_Y; w,h: 0 = 256. Без скобки и diff --git a/libbgi/bgi256/_bgi_blit_cols_raw.c b/libbgi/bgi256/_bgi_blit_cols_raw.c new file mode 100644 index 0000000..f0791aa --- /dev/null +++ b/libbgi/bgi256/_bgi_blit_cols_raw.c @@ -0,0 +1,84 @@ +/* + * _bgi_blit_cols_raw — accel block-copy КОЛОНКАМИ (буфер column-major -> + * экран). Парный к _bgi_blit_rows_raw, но вертикальный проход: каждая + * КОЛОНКА копируется одним accel-burst'ом вниз (Port_Y авто-шагает), + * опкод LD A,A (0x7F — «копирование блока для вертикального прохода», + * см. docs/reference/accel_r.txt). dst двигается на +1 (следующий x) на + * колонку; Port_Y сбрасывается в y0 перед каждой колонкой (accel шагает + * его внутри burst'а); src двигается на sstride на колонку. + * + * ЗАЧЕМ: бесплатный ГОРИЗОНТАЛЬНЫЙ флип персонажей — вызывающий подаёт + * src с конца (src+(w-1)*h) и sstride=-h, тогда правая колонка источника + * ложится в левый x экрана (зеркало). Реверс без CPU и без удвоения + * памяти. Спрайты персонажей хранятся column-major. + * + * ВХОД (__sdcccall(1)): src→HL, dst→DE; стек: w 4(ix), h 5(ix), + * sstride 6/7(ix), y0 8(ix). w — число КОЛОНОК (1..256, 0=256); h — + * высота колонки = размер accel-блока (1..256, 0=256); sstride — шаг + * адреса src между колонками (±); y0 — стартовый Port_Y. Callee-pop 5 Б. + * + * Raw: без клиппинга (вызывающий); W3 замаплен (_bgi_begin/end снаружи); + * банк выставлен вызывающим (0x5C прозрачный / 0x50 обычный). src ВНЕ W3. + * ОДИН DI на весь блит. Колонки > 256 строк режет вызывающий (h ≤ 256). + * Клоббер: AF/BC/DE/HL; IX сохраняется. + */ + +#include "../_bgi.h" + +void _bgi_blit_cols_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0) __naked +{ + (void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0; + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер accel-блока <- h (высота колонки) + ld a, 5 (ix) + ld (bc_len_imm), a + ;; SMC: src-страйд между колонками (8-бит adc-цепочка, ± на вызов) + ld a, 6 (ix) + ld (bc_slo_imm), a + ld a, 7 (ix) + ld (bc_shi_imm), a + ld b, 4 (ix) ; B = число колонок (0 => 256: djnz) + ld c, 8 (ix) ; C = y0 (Port_Y, константа) + di ; один DI на весь блит + ld d, d ; 0x52 — режим размера блока + ld a, #0 + bc_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + bc_col: + ld a, c + out (#0x89), a ; Port_Y = y0 (сброс на верх колонки) + ;; ЧТЕНИЕ — ГОРИЗОНТАЛЬНОЙ копией (LD L,L): src-колонка лежит в ОЗУ + ;; СМЕЖНО, адрес шагает (не port_y). ЗАПИСЬ — ВЕРТИКАЛЬНОЙ (LD A,A): + ;; port_y шагает вниз по видео-колонке. Режим меняется между read и + ;; write (do_accel_block применяет один режим на весь блок). + ld l, l ; 0x6D — гориз. копия (для ЧТЕНИЯ смежных) + ld a, (hl) ; read-блок: h смежных байт src -> буфер accel + ld a, a ; 0x7F — верт. копия (для ЗАПИСИ колонки) + ld (de), a ; write-блок: буфер -> видео-колонка (port_y++) + ld b, b ; 0x40 — стоп + ;; src += sstride (следующая колонка источника) + ld a, l + add a, #0 + bc_slo_imm = . - 1 + ld l, a + ld a, h + adc a, #0 + bc_shi_imm = . - 1 + ld h, a + inc de ; dst += 1 (следующий x) + djnz bc_col + ei ; закрыть DI-окно + pop ix + ;; callee-pop 5 байт (w,h,sstride:2,y0) + pop hl ; ret-адрес + pop af + pop af + inc sp + jp (hl) + __endasm; +} diff --git a/libbgi/common/atlas_load.c b/libbgi/common/atlas_load.c index a2dd543..9565ebe 100644 --- a/libbgi/common/atlas_load.c +++ b/libbgi/common/atlas_load.c @@ -23,7 +23,7 @@ int atlas_load(atlas_t *a, const char *path) { uint8_t *pg = (uint8_t *)0xC000; - uint8_t saved_w3, blk, ok; + uint8_t saved_w3, blk, ok, count; uint16_t stub; int fd, n; @@ -45,8 +45,9 @@ int atlas_load(atlas_t *a, const char *path) n = read(fd, pg, 16384); ok = (n >= (int)_ATL_DATA_OFF && pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1'); + count = 0; if (ok) { - a->count = pg[4]; + count = pg[4]; stub = (uint16_t)&_gfx_w0_isr; pg[0x38] = 0xC3; /* JP _gfx_w0_isr */ pg[0x39] = (uint8_t)(stub & 0xFF); @@ -55,6 +56,13 @@ int atlas_load(atlas_t *a, const char *path) pg[0x67] = 0x45; } + /* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской + * памяти (её адрес тоже резолвится через W3), запись при ещё + * замапленной странице атласа ушла бы не в тот физический банк + * (найдено 2026-07-15 — PoP room.c держит tile_atlas[] в --bank 1, + * a->count оставался мусором, room_draw читал пиксели не с того + * оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не + * касалось, только a->count. */ sprinter_page_w3(saved_w3); close(fd); @@ -64,5 +72,6 @@ int atlas_load(atlas_t *a, const char *path) errno = EINVAL; return -1; } + a->count = count; return 0; } diff --git a/libbgi/common/gfx_blit_cols.c b/libbgi/common/gfx_blit_cols.c new file mode 100644 index 0000000..8f512a8 --- /dev/null +++ b/libbgi/common/gfx_blit_cols.c @@ -0,0 +1,61 @@ +/* + * gfx_blit_cols — блит спрайта column-major (getimage-заголовок uint16 w,h; + * далее пиксели ПО КОЛОНКАМ: колонка 0 сверху вниз, потом колонка 1, ...) + * в (x,y) текущим банком (gfx_set_bank; 0x5C/0x58 прозрачный). Каждая + * колонка — вертикальный accel-burst (_bgi_blit_cols_raw). + * + * flip != 0 — ГОРИЗОНТАЛЬНОЕ зеркало (для направления персонажа): источник + * читается с последней колонки к первой (sstride отрицательный), правая + * колонка ложится в левый x экрана. Без CPU-реверса и без второй копии + * атласа. Зеркало происходит ВНУТРИ футпринта [x, x+w); чтобы держать + * "hot-point" персонажа на месте (как оригинал: xpos -= w), вызывающий + * при флипе подаёт x = anchor - w (см. pop_kid.c kid_draw). + * + * Клип по экрану ЕСТЬ (аналог _gfx_blit_full, но роли осей зеркальны: + * x = номер КОЛОНКИ, y = смещение ВНУТРИ колонки). sstride считается по + * ПОЛНОЙ высоте (колонки в ОЗУ лежат full_h друг от друга), длина accel- + * блока — по обрезанной. Спрайты Kid < 256 по обеим осям → нарезка на + * полосы >256 не нужна (raw принимает uint8_t w,h; 0 => 256). img вне W3. + */ +#include +#include "../_bgi.h" + +void gfx_blit_cols(int x, int y, const void *img, uint8_t flip) +{ + const uint8_t *p = (const uint8_t *)img; + int full_w = p[0] | (p[1] << 8); + int full_h = p[2] | (p[3] << 8); + const uint8_t *px = p + 4; + int w = full_w, h = full_h; + int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */ + int sy = 0; /* пропуск строк сверху колонки */ + int cx = 0; /* пропуск колонок слева экрана */ + const uint8_t *src; + uint8_t *dst; + + if (full_w <= 0 || full_h <= 0) + return; + + /* Клип по экрану — только если хоть один край вылезает. */ + if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { + if (x < 0) { cx = -x; w += x; x = 0; } + if (y < 0) { sy = -y; h += y; y = 0; } + if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x; + if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y; + if (w <= 0 || h <= 0) return; + } + + /* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. + * cx экранных колонок слева пропускаем шагом sstride (в флипе он + * отрицателен — уходим к более ранним колонкам источника); sy строк + * сверху — простым +sy внутри колонки (порядок строк флип не меняет). */ + src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px; + if (cx) + src += cx * sstride; + src += sy; + + _bgi_begin(); + dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x); + _bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y); + _bgi_end(); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index fc61af9..a3b2445 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -93,6 +93,13 @@ void gfx_blit(int x, int y, const void *img); void gfx_blit_part(int x, int y, const void *img, int sx, int sy, int w, int h); +/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel- + * проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без + * CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot- + * point» подавай x=anchor-w при флипе). Клип по экрану есть. + * Для персонажей (флип); фон — обычный gfx_blit (row-major). */ +void gfx_blit_cols(int x, int y, const void *img, uint8_t flip); + /* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank; diff --git a/libc/conio/kbd_mod_state.c b/libc/conio/kbd_mod_state.c new file mode 100644 index 0000000..4308e8d --- /dev/null +++ b/libc/conio/kbd_mod_state.c @@ -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_* в . + */ + +#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; +} diff --git a/libc/include/conio.h b/libc/include/conio.h index fd004c0..61e1246 100644 --- a/libc/include/conio.h +++ b/libc/include/conio.h @@ -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 не имеют, см. . + * + * Возврат: (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 { diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h new file mode 100644 index 0000000..ee71f29 --- /dev/null +++ b/libc/include/kbd_raw.h @@ -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() () — тот же приватный 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 + +/* Включить 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 diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h index cb303f3..b034093 100644 --- a/libc/irq/_irq.h +++ b/libc/irq/_irq.h @@ -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-уборку; diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c index 4f8b9ca..7028d4f 100644 --- a/libc/irq/_irq_tramp.c +++ b/libc/irq/_irq_tramp.c @@ -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 клавиатуру не видит вообще (см. ). * - 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:: diff --git a/libc/kbd/_kbdraw.h b/libc/kbd/_kbdraw.h new file mode 100644 index 0000000..5567280 --- /dev/null +++ b/libc/kbd/_kbdraw.h @@ -0,0 +1,26 @@ +/* + * _kbdraw.h — внутренности raw-канала клавиатуры (НЕ публичный + * заголовок; публичный интерфейс — ). + * + * Всё состояние — плоские глобалы (живут в W2/_DATA во ВСЕХ memory- + * режимах, см. sprinter_memory_modes) — трамплин прерывания + * (libc/irq/_irq_tramp.c) обращается к ним БЕЗ W1-remap и без call, + * поэтому раскладка памяти приложения тут ни при чём. + */ +#ifndef _KBDRAW_INTERNAL_H +#define _KBDRAW_INTERNAL_H + +#include + +/* Биты _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 diff --git a/libc/kbd/_kbdraw_state.c b/libc/kbd/_kbdraw_state.c new file mode 100644 index 0000000..837d680 --- /dev/null +++ b/libc/kbd/_kbdraw_state.c @@ -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]; diff --git a/libc/kbd/kbd_raw_close.c b/libc/kbd/kbd_raw_close.c new file mode 100644 index 0000000..588d996 --- /dev/null +++ b/libc/kbd/kbd_raw_close.c @@ -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(); +} diff --git a/libc/kbd/kbd_raw_down.c b/libc/kbd/kbd_raw_down.c new file mode 100644 index 0000000..9f5889f --- /dev/null +++ b/libc/kbd/kbd_raw_down.c @@ -0,0 +1,14 @@ +/* + * kbd_raw_down — зажата ли клавиша code ПРЯМО СЕЙЧАС (0/1). + * Читает битовую карту, которую ведёт трамплин прерывания + * (libc/irq/_irq_tramp.c) — O(1), никаких ESTEX-вызовов. + */ + +#include +#include "_kbdraw.h" + +uint8_t kbd_raw_down(uint16_t code) +{ + if (code >= 512) return 0; + return _kbdraw_down[code]; +} diff --git a/libc/kbd/kbd_raw_open.c b/libc/kbd/kbd_raw_open.c new file mode 100644 index 0000000..ea31500 --- /dev/null +++ b/libc/kbd/kbd_raw_open.c @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/tests/kbdraw/Makefile b/tests/kbdraw/Makefile new file mode 100644 index 0000000..ac24473 --- /dev/null +++ b/tests/kbdraw/Makefile @@ -0,0 +1,5 @@ +# Build kbdraw.exe — raw-канал клавиатуры (), TINY memory mode. + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := kbdraw +include $(PROJ_ROOT)/app.mk diff --git a/tests/kbdraw/kbdraw.c b/tests/kbdraw/kbdraw.c new file mode 100644 index 0000000..96bf0d6 --- /dev/null +++ b/tests/kbdraw/kbdraw.c @@ -0,0 +1,65 @@ +/* + * kbdraw — тест raw-канала клавиатуры (). Держит на + * экране live-состояние нескольких клавиш плюс список ВСЕХ кодов, + * которые движок сейчас считает зажатыми (для калибровки KBD_* — + * см. PORT_PLAN.md §2: значения там взяты по общеизвестному стандарту + * PS/2 Set 2, не подтверждены на этом железе/MAME до этого теста). + * Заодно проверяет, что kbd_mod_state() (CTRLKEY, зависит от + * резидентного обработчика DSS) не обновляется, пока raw открыт — + * ожидаемое следствие §2 ("DSS не видит клавиатуру, пока raw + * активен"). + * + * Выход — ESC через raw-канал (kbd_raw_down(KBD_ESC)), НЕ через DSS. + */ +#include +#include +#include +#include + +static void show(uint8_t row, const char *name, uint8_t down) +{ + gotoxy(0, row); + printf("%-8s %s ", name, down ? "DOWN" : "up "); +} + +int main(void) +{ + uint16_t c; + + clrscr(); + puts("kbdraw test: hold arrows/space/shift; ESC to quit"); + + if (kbd_raw_open() != 0) { + puts("kbd_raw_open failed"); + return 1; + } + + for (;;) { + show(2, "LEFT", kbd_raw_down(KBD_LEFT)); + show(3, "RIGHT", kbd_raw_down(KBD_RIGHT)); + show(4, "UP", kbd_raw_down(KBD_UP)); + show(5, "DOWN", kbd_raw_down(KBD_DOWN)); + show(6, "SPACE", kbd_raw_down(KBD_SPACE)); + show(7, "LSHIFT", kbd_raw_down(KBD_LSHIFT)); + show(8, "RSHIFT", kbd_raw_down(KBD_RSHIFT)); + show(9, "ESC", kbd_raw_down(KBD_ESC)); + + gotoxy(0, 11); + printf("mod=%04X (expected frozen while raw is open) ", + kbd_mod_state()); + + gotoxy(0, 13); + printf("active codes: "); + gotoxy(14, 13); + for (c = 0; c < 512; c++) + if (kbd_raw_down(c)) printf("%03X ", c); + + if (kbd_raw_down(KBD_ESC)) break; + delayms(60); + } + + kbd_raw_close(); + clrscr(); + puts("raw closed, DSS keyboard restored"); + return 0; +}