# libc — справочник API (2026-07-06)

Сводка по заголовкам: сигнатура + одна строка + особенности ABI.
Детали дизайна: docs/libc-headers.md (контракт затенения SDCC),
docs/file-buffering-design.md (FILE*), docs/solid_c_compatibility.md.

Общие соглашения:
- ошибки: возврат -1/NULL/EOF + `errno` (код DSS as-is, см. errno.h);
- SDCC `__sdcccall(1)`: 1-й аргумент HL (8-битный — A), 2-й — DE,
  остальные на стеке; **int/указатель возвращается в DE**;
- строки для BIOS-вызовов (rst 8) должны лежать в #4000–#BFFF;
- стек при любых ESTEX/BIOS-вызовах — в W2 (обеспечено crt0).

## <stdio.h> — полная замена SDCC (контракт: printf-семейство из z80.lib)

Из SDCC z80.lib: `printf sprintf vprintf vsprintf`.

Консоль (ESTEX, без атрибутов — быстрый путь; цветной вывод — conio):
| Сигнатура | Описание |
|---|---|
| `int putchar(int c)` | символ через PUTCHAR $5B; '\n'→CR LF |
| `int getchar(void)` | блокирующий WAITKEY $30, ASCII |
| `char puts(const char *s)` | строка + '\n' (посимвольно через putchar) |
| `char *gets(char *buf)` | строка с консоли, без контроля длины |
| `void dec8/dec16/dec32(v)` | десятичная печать без ведущих нулей |
| `void hex8/hex16/hex32(v)` | hex-печать фиксированной ширины |

FILE* (буферизованный, вариант B+ — единый ленивый буфер BUFSIZ=512
на чтение/запись с автопереключением; таблица `OPEN_MAX=8` слотов;
exit() сбрасывает всё через atexit; **ошибки записи отложенные —
проверять fclose**):
| Сигнатура | Описание |
|---|---|
| `FILE *fopen(path, mode)` | "r/w/a" + '+', 'b/t' игнорируются |
| `FILE *fdopen(fd, mode)` | завернуть готовый fd (закрывать fclose!) |
| `FILE *freopen(path, mode, fp)` | переоткрыть тот же FILE* |
| `int fclose(FILE*)` / `void fclosall(void)` | сброс+закрытие / все потоки |
| `int fflush(FILE*)` | сброс записи / откат readahead; NULL = все |
| `int fgetc/fputc(...)` | горячий путь на asm; getc/putc — макро-алиасы |
| `char *fgets(buf, n, fp)` | до '\n' (сохраняется); блочный LDI-сканер |
| `int fputs(s, fp)` | без '\n'; через fwrite |
| `size_t fread/fwrite(p, sz, n, fp)` | блоки ≥ 512 идут мимо буфера |
| `int ungetc(c, fp)` | 1 байт putback (и на stdin) |
| `int fseek(fp, off, whence)` / `long ftell(fp)` | ftell без побочных эффектов |
| `void rewind(fp)` | fseek(0) + сброс EOF/ERROR |
| `int fgetpos/fsetpos(fp, &pos)` | fpos_t = long |
| `int feof/ferror(fp)`, `void clearerr(fp)` | флаги потока |
| `int fprintf/vfprintf(fp, fmt, ...)` | vsprintf в статический буфер 256 |
| `int scanf/fscanf/sscanf(...)` | %d %u %x %o %c %s, `l`, ширина, %*, %% |
| `int rename(old, new)` | ESTEX RENAME $10 |

`stdin/stdout/stderr` — консольные псевдопотоки (fd 0/-1/-2), не
буферизуются; freopen на них меняет только FILE*-операции (printf
идёт в ESTEX напрямую).

## <stdlib.h> — include_next + добавки

Из SDCC: malloc/free/calloc/realloc (heap в W2), atoi/atol/strtol/
strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv, exit-типы.
Наше: `int16_t min(a,b)`, `int16_t max(a,b)` (функции, как в Solid-C).

## <string.h> — include_next + добавки

Из SDCC: mem*/str* полностью. Наше: `char *strlwr/strupr(char *s)` —
in-place регистр, латиница + кириллица CP866.

## <time.h> — полная замена (struct tm в SDCC-ABI, __TIME_UNSIGNED=1)

| Сигнатура | Описание |
|---|---|
| `void getdatetime(datetime_t*)` | RTC как есть (ESTEX SYSTIME $21) |
| `int setdatetime(const datetime_t*)` | установка RTC ($22) |
| `time_t time(time_t*)` | Unix-эпоха из RTC |
| `mktime/gmtime/localtime/asctime/ctime` | POSIX поверх RTC (без TZ) |

`datetime_t`: day/month/year(полный)/hour/minute/second/dow (1=Вс).

## <unistd.h>, <fcntl.h> — fd-уровень (манипуляторы DSS)

| Сигнатура | Описание |
|---|---|
| `int open(path, flags)` | O_RDONLY/WRONLY/RDWR + O_CREAT/TRUNC/EXCL/APPEND (ESTEX $11/$0A/$0B) |
| `int creat(path, mode)` | open(W|CREAT|TRUNC); mode игнорируется |
| `int read/write(fd, buf, n)` | ESTEX $13/$14. **Квирк WRITE: DE-возврат ненадёжен, успех = CF=0&A=0** (см. memory/estex_write_de_quirk) |
| `int close(fd)` | ESTEX $12 |
| `long lseek(fd, off, whence)` | 32-битная позиция (MOVE_FP $15) |
| `int unlink(path)` | удалить (DELETE $0E) |
| `int isatty(fd)` | fd <= 0 (файловые манипуляторы DSS с 1) |
| `int mkdir/rmdir/chdir(path)` | ESTEX $1B/$1C/$1D |
| `char *getcwd(buf, size)` | буфер 256 байт, size игнорируется |
| `void sleep(seconds)` | калиброванный busy-wait (см. ниже) |
| `void delayms(uint16_t ms)` | то же, гранулярность — миллисекунды (см. ниже) |

**Лимит: 8 одновременных манипуляторов**; 9-й OPEN вешает DSS —
libc отказывает сама (EMFILE, предохранитель _fd_guard).

**`sleep()` (2026-07-07, редизайн)**: старая версия считала halt-
пробуждения (50 = 1 c), предполагая, что КАЖДОЕ прерывание — кадровый
тик; с CBL/клавиатурой на векторе 0xFF это уже не так (CBL прерывает
намного чаще кадра — sleep() возвращался бы раньше срока). Теперь:
лениво, один раз калибруется кратковременным `irq_install()` против
РЕАЛЬНОГО кадрового тика (трамплин зовёт хук только на настоящих
кадровых прерываниях — клавиатура/CBL уходят в свои ветки раньше),
считая, сколько итераций тесного цикла умещается в один КАДР (не в
секунду — калибровка на секунду переполняла `uint16_t`, был баг:
`sleep(5)` отрабатывал быстрее секунды, найден пользователем на
реальном прогоне и исправлен). `sleep(seconds)` — вложенный цикл:
внешний по секундам, внутренний ровно 50 раз калиброванный busy-wait —
без единого прерывания и БЕЗ умножения/32-битной арифметики (по духу
`docs/samples/delayms.asm`, который тоже калибрует на 1 мс, а не на
1 с). Погрешность ~5-10% (калибровочный и рабочий циклы не тактово-
идентичны). Fallback на старое "50 halt" поведение, если фрейм-хук уже
занят другим `irq_install()`-клиентом (EBUSY).
**Cтоит дороже по размеру** (~+580 Б) — тянет весь модуль `irq`
(install/remove/трамплин/IM2-таблицу), даже если программа больше
ничего из irq не использует.

**`delayms(ms)`** — тот же движок (`libc/time/_sleep_calib.c`), общий с
`sleep()`: калибровка одна на двоих (первый вызов ЛЮБОЙ из функций
калибрует, вторая просто использует готовое). Производная величина
"итераций на 1 мс" — одно 16-битное деление (`__divuint`, НЕ
`__mullong`) на константу 20, вычисляется один раз при калибровке;
`delayms(ms)` — простой цикл `ms` раз, без умножения вообще. Fallback
при EBUSY — грубый (одно кадровое `halt`), точной альтернативы для
миллисекундной гранулярности без калибровки нет.

**Квирк общий для sleep()/delayms()**: калибровка сама стоит ~20-40 мс
(синхронизация на границу кадра + сам замер) — ПЕРВЫЙ вызов ЛЮБОЙ из
двух функций в программе превысит запрошенное время на эту величину;
для `delayms()` с маленьким `ms` это заметно (`delayms(5)` на первом
вызове может растянуться на ~25-45 мс). Все последующие вызовы точны.
Оставлено как есть по решению пользователя — не стали усложнять API
отдельным calibrate()-примитивом.

## <errno.h>

`errno` (int), коды = коды DSS (EOK..EUNKERR) + POSIX-имена
(ENOENT/EBADF/EMFILE/…) + алиасы Solid-C (EZERO/EINVFNC/ENOFILE/…).
`const char *strerror(int)`, `void perror(const char*)`.

## <dos.h> — DOS-слой Solid-C

| Сигнатура | Описание |
|---|---|
| `void getdate/gettime(&d)` | struct date/time (Turbo-C; ti_hund=0) |
| `int setdate/settime(&d)` | RMW полного datetime |
| `uint8_t getdisk(void)` | текущий диск, 0=A (ESTEX $02) |
| `int setdisk(uint8_t)` | смена диска; возврат = число дисков ($01) |
| `int absread/abswrite(disk, sect, cnt, buf)` | секторы ЛОГИЧЕСКОГО диска (BIOS $55/$56); буфер в #4000–#BFFF; abswrite минует ФС! |

## <dir.h>

`int ffirst(pattern, ffblk_t*, attrib)` / `int fnext(ffblk_t*)` —
поиск по шаблону (ESTEX $19/$1A). Квирк: "."/".." находятся только
итерацией "*.*" (memory/estex_ffirst_dotdot). FA_*-атрибуты.

## <sys/stat.h>

`int stat(path, struct stat*)` / `int fstat(fd, ...)` — st_mode
(S_ISREG/S_ISDIR), st_size, st_mtime (Unix-эпоха).

## <conio.h> — текстовый экран с атрибутами (Turbo-C стиль)

Клавиатура: `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 нет, см. `<kbd_raw.h>`.
Вывод с атрибутом: `putch cputs cprintf` (~10× медленнее stdio-пути;
'\n' НЕ транслируется — писать "\r\n").
Атрибуты: `textcolor textbackground textattr`, `set/get_text_attr`,
COLOR_*-enum, `COLOR(fg,bg)`, COLOR_BLINK; `set/get_putch_raw_mode`.
Экран: `clrscr clrscr_attr gotoxy home() wherex wherey wherexy scroll
wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32,
0x03=80×32).
Порты/IRQ: `inp outp enable() disable()`.
Текстовая палитра: `text_pal_load/set_color/get/get_color/reset`
(план 0..3 → страница BIOS 4..7).

## <bios/text.h> — быстрый BIOS-вывод (rst 8, place-based)

`bios_set_place/get_place`, `bios_write[attr][_until|_stop]`,
`bios_fillchar/fillattr/fillcharattr`, `bios_clearwin[_ch]`,
`bios_scrollwin`. Строка s — в #4000–#BFFF; place продвигается.

## <gfx.h> — графика, mode-agnostic API (BGI_GFX); живёт в libbgi/include

Рисование (putpixel/line/bar/circle/…) вынесено в BGI — см. `<graphics.h>`
и driver-библиотеки lib/bgi256.lib / lib/bgi16.lib (выбор режима линковкой:
`sprinter-cc --gfx 256` / `--gfx 16`).  В gfx.h остались только функции
БЕЗ BGI-аналога (mode-agnostic, живут в libbgi/common/, .rel в обеих
 driver-библиотеках):

Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
(double buffering), `gfx_set/get_bank` (GFX_BANK_NORMAL 0x50 /
NOSHADOW 0x54 / TRANSPARENT 0x58 / SPRITE 0x5C — аппаратные подрежимы
записи, действуют на ВСЕ примитивы; см. docs/sprite-api-design.md).
Блиттинг (Фаза B, 2026-07-11): `gfx_blit(x,y,img)`,
`gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас), `gfx_heal(x,y,w,h)`
(восстановить фон из ОЗУ-копии; стирание спрайтов без save-буфера);
img — getimage-формат, буфер вне W3; клиппинг по экрану есть.

`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
кадра, см. MAME sprinter.cpp kbd_fe_r), а не прерывание — поэтому не
путается с клавиатурой/CBL/CTC, деляющими вектор 0xFF.  Бит доступен
только пока включён `cbl_mode()` (bit7 порта 0x004E) — если приложение
уже играет через `cbl_open()`, бит достаётся бесплатно; иначе
`gfx_wait_vsync()` лениво занимает bit7 "немым" кодом частоты через
`_cbl_port_ref()`/`_cbl_port_unref()` (см. `<cbl.h>`, `_cbl_port.c`) —
разделяемое владение портом 0x004E, безопасное при любом порядке
использования с реальным CBL-звуком.  Фолбэк на одно кадровое
прерывание (`halt`), если бит не ведёт себя как ожидается за разумное
число попыток.
Шрифт: `gfx_load_default_font`, `gfx_set_font(ptr)` (interleaved
font[row*256+char]) — грузится лениво при первом использовании BGI-текста.
Палитра: `gfx_pal_load/set/get/get_color/reset` (страницы 0..3).

Константы режимов: `GFX_MODE_320x256x256` (0x81), `GFX_MODE_640x256x16`
(0x82); размеры `GFX_WIDTH/HEIGHT` (320/256), `GFX_WIDTH_16/HEIGHT_16`
(640/256).  Рисование через `<graphics.h>` (BGI).

## <graphics.h> — Turbo-C BGI (функц. совместимость), Фаза 1, режим 256

Слой поверх `<gfx.h>` со «текущим» цветом/позицией.  Режим задаётся
driver-либой на линковке: `sprinter-cc --gfx 256` (→ 320×256×256; 16 —
позже).  API mode-agnostic: код не меняется при смене режима.
Setup: `initgraph()` (без аргументов — режим фиксирован либой; грузит
EGA-палитру 0..15, цвет=WHITE, фон=BLACK, CP=(0,0)),
`closegraph()`, `graphresult()`, `cleardevice()`.
Границы/цвет: `getmaxx/getmaxy` (319/255), `getmaxcolor` (255),
`setcolor/getcolor`, `setbkcolor/getbkcolor`.  Константы BLACK..WHITE.
Точки: `putpixel(x,y,c)`, `getpixel(x,y)`.
Позиция/линии: `moveto/moverel/getx/gety`, `lineto/linerel` (двигают
CP), `line(x1,y1,x2,y2)` (не двигает).
Фигуры: `rectangle` (контур), `bar` (заливка стилем), `circle`.
Дуги (Ф2a): `arc`, `ellipse(x,y,st,end,xr,yr)`, `drawpoly(n,pts)`.
Текст 8×8: `outtextxy(x,y,s)`, `outtext(s)` (двигает CP).
Заливки (Ф2b): `setfillstyle(pattern,color)`/`getfillsettings` (10
паттернов Borland: SOLID/EMPTY/LINE/…/HATCH/XHATCH/…), `bar3d`,
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
Образы (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
формат буфера: uint16 w,h + w*h байт).  COPY_PUT и getimage — через
accel block-copy (2026-07-11, Фаза A sprite-api-design), COPY с
клиппингом и текущим банком; XOR/OR/AND/NOT — per-pixel.
Спрайты (2026-07-11, Фаза B sprite-api-design): `putsprite(x,y,img)` —
блит getimage-буфера с аппаратной прозрачностью (0xFF =
GFX_TRANSPARENT не пишется) банком GFX_BANK_SPRITE (0x5C, фон в
ОЗУ-копии цел); `movesprite(ox,oy,x,y,img)` — heal старой позиции +
putsprite новой (save-буфер не нужен).  Низкий уровень в <gfx.h>:
`gfx_blit(x,y,img)`, `gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас
кадров), `gfx_heal(x,y,w,h)` (восстановление фона из ОЗУ-копии),
константы GFX_BANK_NORMAL/NOSHADOW/TRANSPARENT/SPRITE.  Правило: фон
рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы
образов — вне W3 (< 0xC000).  Тесты: tests/sprites, tests/gfxbanks,
tests/bgi_img.
Скролл региона из НЕактивной страницы в активную (<sprite.h>):
`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый
построчный accel-скролл без страйдов, DI-банды по 16 строк),
`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; через
строку-буфер на стеке, ПОКА не оптимален — Port_Y один на burst, разный
read/write Y невозможен без ломающего accel OUT).  Копия = скролл + heal
цели (банк 0x50).  Открывшуюся полосу |d| не заполняют — возвращают в
*dirty (NULL = не нужно).  Пример: examples/scroll.
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
drawpoly.
Стиль текста (Ф2d): `settextstyle(font,dir,size)`/`gettextsettings`,
`textwidth`/`textheight` — масштаб 1..10, HORIZ/VERT, прозрачный фон
(только DEFAULT_FONT 8×8).
Реализация: libbgi/common/*.c (mode-agnostic math + BGI public API +
BGI_GFX) + libbgi/bgi256/*.c (256-цветные leaf'ы) → lib/bgi256.lib
(Фаза 2 добавит libbgi/bgi16/*.c → lib/bgi16.lib; common .rel одни и
те же в обоих архивах).  Leaf'ы — реальные реализации (БЕЗ обёрток:
putpixel/getpixel полностью inline, _bgi_plot_raw/_bgi_hspan_raw/...
поглощают акселераторный asm).  Пакетные примитивы — одна W3-скобка
на примитив; тригонометрия/эллипсы целочисленные (Q7/isqrt, БЕЗ 32-бит).
Cross-lib: libbgi всегда линкуется с libc; _cbl_port_ref/unref
объявлены extern в libbgi/_bgi.h (сверять с libc/cbl/_cbl.h).
Ф2d (осталось): setviewport/клиппинг, settextjustify, setaspectratio —
см. docs/TODO.md.

## <irq.h> — user-ISR кадрового прерывания (IM 2)

| Сигнатура | Описание |
|---|---|
| `int irq_install(isr_t h)` | h зовётся ~50 Гц на кадровых прерываниях; DSS-обработчик чейнится всегда (клавиатура/SYSTIME/мышь живы). 0 / -1+errno (EBUSY повтор, EINVAL — код не в W2: только tiny/big) |
| `void irq_remove(void)` | вернуть таблицу DSS; идемпотентно; висит на atexit |
| `int irq_ctc_install(h, div2, div3)` | периодический таймер CTC (вектор 0x06): f = 875000/(div2×div3), div 0=256; пресет кадра IRQ_CTC_VSYNC_DIV2/3 (112×160, ~48.8 Гц); независим от кадрового; трамплин завершает RETI |
| `void irq_ctc_remove(void)` | глушит CTC (обязательно; atexit подстрахует) |
| `IRQ_DISABLE()/IRQ_ENABLE()` | di/ei — скобки для чтения shared-переменных из main |

Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор,
banked-функции; только volatile-глобалы и быстрая работа (<1 мс).

## <kbd_raw.h> — эксклюзивный 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 |
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
| `uint8_t kbd_raw_poll(void)` | вычерпать FIFO ОПРОСОМ, не дожидаясь прерывания: 0 — было пусто (~40 тактов), 1 — что-то декодировано. Звать МЕЖДУ фазами кадра, после тяжёлых блитов — не раз в кадр (см. ниже, «почему одного прерывания мало») |
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |

**ИСПРАВЛЕНО 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_poll`, 2026-08-01).**  Приёмный
FIFO SIO — 3 байта, и рассчитывать на «каждый байт разбудит нас» нельзя:
запрос прерывания клавиатуры держится единицы микросекунд (в dev-MAME —
32 такта CPU, `sprinter.cpp` `on_kbd_data` → `irq_off_timer`), а ядра
акселератора держат `DI` на весь блит — сотни микросекунд.  Импульс,
попавший в такое окно, теряется насовсем; байт лежит в FIFO до следующего
прерывания (следующий байт либо кадровое, 50 Гц).  Трафик же идёт пачками:
тап стрелки = 5 байт, а при УДЕРЖИВАЕМОМ Shift PS/2 обрамляет расширенный
код «фиктивным шифтом» (`E0 F0 12` … `E0 12`) — по 5 байт и на нажатие, и
на отпускание.  Три ячейки такую пачку не держат: потерянный make = «нажатие
не сработало», потерянный break = залипание.  Отсюда `kbd_raw_poll()` —
вычерпывание тем же декодером, но из главного цикла.  Тело идёт под `DI`
(чтение порта 0x18 деструктивно, а read-modify-write карты гонится с
трамплином) и БЕЗУСЛОВНО делает `EI` на выходе — из ISR звать нельзя.

**Как её звать (замеры 2026-08-01, PoP roomtest — читать до применения!).**
Расстановка «несколько вызовов за кадр, после тяжёлых фаз» **не даёт
ничего**: 9 дошедших make-байт из 10 нажатий и с ней, и без неё — такие
вызовы попадают ровно в участки с разрешёнными прерываниями и лишь
дублируют трамплин.  Там же измерено, что длина DI-окон графики на потери
НЕ влияет (в кадре вообще без блитов потерь больше), а теряется байт ДО
чтения порта: примерно 44 % импульсов запроса прерывания не обслуживаются,
и трёхбайтовый FIFO переполняется.

**Работает только ПЛОТНЫЙ опрос — порядка раза в 0.5 мс.**  Штатный способ
взять эту частоту даром — повесить функцию idle-хуком графики:
`gfx_set_idle_hook()` (`<gfx.h>`) зовёт её, пока `gfx_wait_vsync` крутит
опрос луча, а это ~2/3 периода кадра.  Проверено: 35 нажатий стрелки с
зажатым Shift → 35 дошедших make против 9 из 10 без хука.  Остаточные
редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода);
полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS.md`,
задача KBD-1.

**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `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.h> — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR)

Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер
256 Б (2×128, двойная буферизация на стороне железа — см. официальную
доку "5.3 COVOX-Blaster"); библиотека просто зовёт `fill()` приложения
ИЗ ISR, а оно само пропихивает данные (откуда угодно) через
`cbl_push_otir/accel` — без промежуточной копии.

| Сигнатура | Описание |
|---|---|
| `int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) |
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
| `uint16_t cbl_requests(void)` | счётчик запросов блока ISR (~fs/(сэмплов в блоке) в секунду) |
| `uint16_t cbl_underruns(void)` | счётчик недоливов (`fill` вернул 0 или не задан) |
| `CBL_FREQ_7K8 .. CBL_FREQ_109K` | коды частоты (биты 3..0 control-порта) |
| `CBL_FMT_MONO8/MONO16/STEREO8/STEREO16` | формат (биты 5/6 control-порта); блок 128 Б (8-бит) или 256 Б (16-бит), не зависит от моно/стерео; тишина 0x80 (8-бит) / 0x0000 (16-бит, знаковый) |
| `CBL_PUMP_OTIR / CBL_PUMP_ACCEL` | способ выдачи — см. ниже |
| `CBL_UNDERRUN_APP / CBL_UNDERRUN_SILENCE` | поведение при недоливе — см. ниже |

`typedef int (*cbl_fill_fn)(uint16_t n);` — callback, зовётся ИЗ ISR за
очередным блоком (n = `_cbl_block`, 128/256).  **Обязан быть быстрым**
— никаких ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у
`irq_install()`-хендлера); вернуть ненулевое, если реально пропихнул n
байт.  Диск (read()) читать из fill() НЕЛЬЗЯ — см. tests/cblstream,
где под это заведено кольцо уровня приложения.

Два насоса (3-й параметр `cbl_open`):
- **OTIR** (`_cbl_pump_otir`) — `cbl_push_otir()` в порт 0x4F; базовый.
  **НЕ умеет 16-бит** — `cbl_open(..., MONO16/STEREO16, CBL_PUMP_OTIR,
  ...)` вернёт EINVAL: по исходнику MAME (sprinter.cpp) порт данных
  ВСЕГДА кладёт байт как есть в один слот, не собирая пару байт в
  16-бит значение и не сверяясь с 16-бит флагом вообще.
- **ACCEL** (`_cbl_pump_accel`) — `cbl_push_accel()` через акселератор
  в спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (см.
  docs/converted/accel_r.txt, Forum.txt); размер блока патчится SMC
  (`LD D,D` + immediate `LD A,n` + `LD L,L` — тот же паттерн, что и в
  libc/gfx/_gfx_hfill256.c); единственный путь для 16-бит.
  **Verified в MAME 2026-07-07** (tests/cbltest, вся accel-половина
  матрицы прошла без ошибок/underrun).

Поведение при недоливе (4-й параметр `cbl_open`):
- **CBL_UNDERRUN_APP** (по умолчанию, 0) — не забота библиотеки, буфер
  тишины НЕ аллоцируется, в CBL доигрывает то, что уже лежало в его
  аппаратном буфере.
- **CBL_UNDERRUN_SILENCE** (1) — насос сам пропихивает тишину; буфер
  (128/256 Б по формату) аллоцируется malloc'ом ВНУТРИ `cbl_open()`
  только в этом режиме.
`cbl_underruns()` считает недоливы в обоих режимах — диагностика,
поведение не меняет.

Приватное прерывание, к DSS не чейнится; бит 7 порта 0xFE (запрос
блока) читается только пока `cbl_open` не закрыт (при выключенном CBL
бит всегда 1 — MAME-квирк).

**Разделяемое владение портом 0x004E** (2026-07-07): бит 5 порта 0xFE
(позиция луча — см. `<gfx.h>` `gfx_wait_vsync()`) доступен только пока
включён bit7 порта 0x004E, независимо от того, играет ли реальный
звук.  `_cbl_port_ref()`/`_cbl_port_unref()` (internal, `_cbl_port.c`)
дают gfx-модулю занять bit7 "немым" кодом частоты (не заводящим таймер
CBL — без звука/прерываний), не мешая реальной `cbl_open()`-сессии,
если она уже идёт (и наоборот — `cbl_close()` возвращает "немой" режим
вместо полного выключения порта, если gfx его ещё держит).

## <palette.h> — низкий уровень (BIOS $A4/$A6)

`pal_load pal_get pal_set_color pal_get_color` (страница 0..7,
записи B,G,R,0), `pal_reset(type)` / `pal_reset_at(type,page,graph)`;
PAL_GRAPH/PAL_SINCLAIR/PAL_CGA.

## <mouse.h> — драйвер RST 30h

`mouse_init show hide refresh read(mouse_state_t*) goto bounds_x/y
text_cursor load_cursor/get_cursor(mouse_cursor_t*) set_sensitivity
get_sensitivity_x/y video_mode_changed`. Sensitivity = делитель
(меньше = быстрее). Solid-C алиасы ms_* включены.

## <sprinter.h> — платформа

Константы портов (PORT_PAGE_W0..W3, PORT_RGADR, PORT_RGMOD), номера
всех ESTEX-функций (ESTEX_*), BIOS EMM ($C0..$C7); `__sfr`-доступ и
inline `sprinter_page_w0..w3(page)`; ENV: `getenv putenv sysenv`.

## <sprinter_mem.h> — EMM-страницы и банковый I/O

`mem_alloc_pages(n)→blk_id, mem_free_block, mem_get_page(blk,idx),
mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор
MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам:
`bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 —
вариант через окно W1).

## <sprinter_exit.h>

`atexit` (LIFO, 8 слотов), `exit` (хендлеры+сброс FILE), `_exit`.

## <sprinter_compat.h> / <sprinter_solid.h>

Типы (BYTE/BOOL/WORD/uint/FD/f_point), TRUE/FALSE/OK/ERROR,
`setmem movmem` (порядок аргументов!), `strerr seek tell ltell
remove _ffirst _setargv abort()`, isascii. `<sprinter_solid.h>` —
зонтичный: один include для портирования Solid-C программ.
