diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index a79bd53..ce69095 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -15,9 +15,9 @@ bios_text 4461 blitperf 5860 blitw 4939 cat 913 -cblstream 5860 -cbltest 6006 -cblwav 6099 +cblstream 5862 +cbltest 6008 +cblwav 6101 conio 4605 conio2 3929 convbench 3508 @@ -28,7 +28,7 @@ fdmax 6023 filetest 10547 fpsdiv 4870 gets 509 -gfx_dbuf 5131 +gfx_dbuf 5142 gfx_demo 4114 gfxbanks 6044 hello 4167 diff --git a/libc/cbl/_cbl.h b/libc/cbl/_cbl.h index c69e050..791952a 100644 --- a/libc/cbl/_cbl.h +++ b/libc/cbl/_cbl.h @@ -26,12 +26,29 @@ * * _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих * держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL- - * сессии. Нужно gfx_wait_vsync() (см. libc/gfx/gfx_wait_vsync.c): бит - * 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp kbd_fe_r), - * но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный CBL уже - * открыт — портом владеет он, ref/unref его не трогают; иначе включают - * bit7 "немым" кодом частоты _CBL_VSYNC_FREQ (таймер CBL не заводится - * — divs[2]=0 в MAME — ни звука, ни прерываний, только синхро-бит). + * сессии. Нужно gfx_wait_vsync() (см. libbgi/common/gfx_wait_vsync.c): + * бит 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp + * kbd_fe_r), но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный + * CBL уже открыт — портом владеет он, ref/unref его не трогают; иначе + * включают bit7 "немым" кодом частоты _CBL_VSYNC_FREQ и заливают + * аппаратный буфер тишиной — БЕЗ ЗАЛИВКИ ЖЕЛЕЗО ПОЁТ (см. ниже). + * + * ЖЕЛЕЗО ПОЁТ ТО, ЧТО ЛЕЖИТ В БУФЕРЕ. Буфер CBL — 256 слотов, которые + * при включённом bit7 крутятся ПО КРУГУ и которые никто не чистит; + * фирменный пример Sprinter Team (docs/converted/Forum.txt, "Самый + * простой пример") на этом и построен: залил 256 отсчётов — "должен + * пищать одну ноту". Значит любое состояние "bit7 включён, а насос не + * работает" — это бесконечная нота из хвоста последнего сэмпла. + * + * В MAME этого не видно: там код частоты 2 не заводил таймер вовсе + * (divs[2]=0, sprinter.cpp:822), поэтому "немой" режим был тих даром. + * На железе код 2 — reserved (Forum.txt:704), поведение не определено, и + * ЦАП тактируется: пользователь поймал бесконечную ноту после Ctrl+S и + * после пропуска заставки (2026-09-02). Поэтому теперь: (1) частота — + * документированная, самая медленная; (2) буфер ВСЕГДА оставляем залитым + * тишиной — и на входе в "немой" режим, и при полном выключении (там те + * же записи центруют ЦАП обычного COVOX, bit7=0 → порт данных идёт прямо + * в ЦАП). Цена — 256 OUT'ов (~0,3 мс) на редкое событие. */ #ifndef _CBL_INTERNAL_H #define _CBL_INTERNAL_H @@ -42,7 +59,10 @@ #define _CBL_CTRL_PORT 0x004E #define _CBL_DATA_PORT 0x4F #define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */ -#define _CBL_VSYNC_FREQ 0x02 /* "немой" код частоты — divs[2]=0 в MAME, таймер не идёт */ +#define _CBL_VSYNC_FREQ 0x08 /* "немой" режим: самый медленный ДОКУМЕНТИРОВАННЫЙ + * код (7,8125 кГц). Тишина обеспечена не + * остановкой таймера (это свойство MAME, не + * железа), а залитым тишиной буфером. */ extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */ extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */ @@ -62,8 +82,10 @@ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode, uint8_t underrun_mode, cbl_fill_fn fill); /* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после - * включения — иначе первые 23 мс играет то, что лежало в буфере раньше. - * Подробный разбор — в шапке _cbl_prime.c. */ + * ЛЮБОЙ записи в порт управления: при включении — иначе первые 23 мс + * играет то, что лежало в буфере раньше; при выключении — иначе железо + * крутит хвост последнего сэмпла бесконечной нотой. Подробный разбор — + * в шапке _cbl_prime.c. */ void _cbl_prime(uint8_t silence); /* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один diff --git a/libc/cbl/_cbl_port.c b/libc/cbl/_cbl_port.c index 01a3c82..973bfa3 100644 --- a/libc/cbl/_cbl_port.c +++ b/libc/cbl/_cbl_port.c @@ -7,9 +7,23 @@ * * Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет * он, ref/unref его не трогают вообще (это уже нужное состояние). - * Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ - * — таймер CBL при этом коде не заводится (divs[2]=0 в MAME), значит - * ни звука, ни прерываний, только доступ к синхро-биту. + * Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ. + * + * ТИШИНУ ДАЁТ ЗАЛИТЫЙ БУФЕР, А НЕ ОСТАНОВЛЕННЫЙ ТАЙМЕР. Раньше здесь + * стоял reserved-код частоты 2 — ровно потому, что в MAME при нём не + * заводится таймер (divs[2]=0). На железе поведение reserved-кодов не + * определено, ЦАП тактируется, и включённый bit7 поверх старого буфера + * даёт бесконечную ноту из хвоста последнего сэмпла (пойман Ctrl+S во + * время музыки, 2026-09-02; разбор — в шапке _cbl.h). Поэтому после + * КАЖДОЙ записи в порт управления заливаем все 256 слотов тишиной: + * - bit7 включён — крутится тишина (беззвучно на любом железе); + * - bit7 выключен — те же записи уходят в ЦАП обычного COVOX и + * центруют его на 0x80, снимая щелчок/постоянное смещение. + * Оба состояния редкие (старт пейсинга, пауза звука, выход), 256 OUT'ов + * — около 0,3 мс. + * + * Цена для ГРАФИЧЕСКОЙ программы без звука: _cbl_prime (~10 Б кода) + * приезжает следом за gfx_wait_vsync. Дешевле, чем гудящее железо. */ #include "_cbl.h" @@ -19,6 +33,9 @@ void _cbl_port_sync(void) { if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */ _cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0); + /* Формат "немого" режима всегда 8-битный (bit5 не выставлен), поэтому + * тишина здесь — 0x80 независимо от того, каким был закрытый сеанс. */ + _cbl_prime(0x80); } void _cbl_port_ref(void) diff --git a/libc/cbl/_cbl_prime.c b/libc/cbl/_cbl_prime.c index 2e39120..841c259 100644 --- a/libc/cbl/_cbl_prime.c +++ b/libc/cbl/_cbl_prime.c @@ -1,5 +1,6 @@ /* - * _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после включения. + * _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после записи в + * порт управления: и при включении, и при выключении. * * ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни * при сбросе, ни при записи в порт управления — там остаётся то, что лежало @@ -31,6 +32,19 @@ * Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый * сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для * 8-бит беззнакового, 0x00 для 16-бит. + * + * ВТОРАЯ РОЛЬ — ГАШЕНИЕ (добавлена 2026-09-02, _cbl_port_sync). Запись + * в порт управления НЕ чистит буфер и в обратную сторону: пока bit7 + * включён (а его держит gfx_wait_vsync ради бита луча), железо крутит + * оставшиеся 256 байт по кругу — это ровно "одна нота" из фирменного + * примера Forum.txt, только незваная. Поэтому после каждой записи в + * порт управления зовём эту же функцию: + * - bit7 остался включён — по кругу крутится тишина; + * - bit7 снят — те же 256 записей идут прямо в ЦАП обычного COVOX + * (MAME sprinter.cpp, `case 0x88` без cbl_mode()) и оставляют его в + * нуле 0x80, а не на последнем сэмпле. + * Гонки с насосом нет и здесь: при гашении хук уже снят, при включении + * первое прерывание будет только на 128-м слоте. */ #include "_cbl.h" diff --git a/libc/cbl/cbl_close.c b/libc/cbl/cbl_close.c index 1e700d2..3275109 100644 --- a/libc/cbl/cbl_close.c +++ b/libc/cbl/cbl_close.c @@ -10,7 +10,16 @@ * Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync() * пересчитывает нужное значение: если gfx_wait_vsync() ещё держит * ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита - * синхронизации; иначе порт гасится полностью. + * синхронизации; иначе порт гасится полностью. Он же заливает + * аппаратный буфер тишиной — БЕЗ ЭТОГО ЗАКРЫТИЕ CBL НЕ ЗАКРЫВАЕТ ЗВУК: + * пока bit7 держит vsync-ссылка, железо крутит по кругу оставшиеся в + * буфере 256 байт последнего сэмпла и поёт ноту до следующего + * cbl_open() (пойман Ctrl+S во время музыки, 2026-09-02; полный разбор + * — в шапке _cbl.h). В MAME это не воспроизводится. + * + * Порядок обязателен: сначала снять хук (иначе насос долил бы буфер уже + * после заливки), и только потом _cbl_port_sync с заливкой — вся + * скобка под DI, чтобы между снятием хука и тишиной ничего не влезло. */ #include "_cbl.h" @@ -21,7 +30,7 @@ void cbl_close(void) if (!_irq_cbl_hook) return; IRQ_DISABLE(); _irq_cbl_hook = 0; + _cbl_port_sync(); /* режим порта + заливка буфера тишиной */ IRQ_ENABLE(); - _cbl_port_sync(); _irq_table_unref(); }