Files
Sprinter-SDCC/docs/libc-reference.md
T
snark13 3d586af031 libc/kbd: raw-клавиатура — вычерпывание FIFO + селективный wipe модификаторов
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
  фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
  клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
  скан-кодов в docs/libc-reference.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:12 +03:00

34 KiB
Raw Permalink Blame History

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
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 + wh байт). 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)
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_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 программ.