cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит
ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу),
bit7 остаётся включённым. А буфер CBL (256 слотов) при включённом bit7
крутится по кругу и не чистится ничем — железо бесконечно поёт хвост
последнего сэмпла, тоном и громкостью по последней мелодии. Пойман
пользователем на железе: Ctrl+S во время музыки и пропуск заставки
давали ноту до следующего cbl_open.
В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у
которого divs[2]==0 и таймер не заводится вовсе. На железе reserved-коды
не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора,
а не железа.
Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА:
- _cbl_port_sync() после каждой записи в порт управления зовёт
_cbl_prime(0x80). Это закрывает и паузу звука, и включение bit7 ради
луча на холодном старте (в буфере лежал мусор от прошлой программы), и
полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП
обычного COVOX и центруют его, снимая щелчок;
- _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8
(7,8125 кГц): поведение определено и на железе, и в MAME, прерывания
по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит
только при живом хуке насоса;
- cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе
насос долил бы буфер уже после заливки.
Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime
следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие —
эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены. Рост atlas в
size-check к этой правке отношения не имеет (замерен тот же и без неё).
Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME
(титры → пропуск заставки → уровень 1, пейсинг по лучу жив). Сам баг
проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Линкер тянет .rel целиком, поэтому malloc/free, стоявшие в мёртвой ветке
CBL_UNDERRUN_SILENCE внутри cbl_open, приезжали в резидент КАЖДОМУ
приложению — включая те, что льют тишину сами и кучей не пользуются.
Разведено:
cbl_open(freq, fmt, pump, fill) — ничего не аллоцирует;
cbl_open_silence(freq, fmt, pump, fill) — аллоцирует буфер тишины;
_cbl_open_raw(...) — общее тело.
Параметр underrun_mode из публичного API убран: режим задаёт выбор функции.
cbl_close больше не зовёт free — иначе malloc возвращался бы тем же путём.
Буфер тишины живёт до выхода из программы и переиспользуется; его размер
запоминается, иначе открытие 16-бит после 8-бит писало бы memset'ом мимо
выделенного куска. _cbl_open_raw указатель на буфер не трогает вовсе —
иначе cbl_open после cbl_open_silence терял бы уже выделенную память.
В дереве режим SILENCE не использовал никто: все три вызова (cblwav,
cblstream, PoP) передавали CBL_UNDERRUN_APP.
Итог для PoP: _CODE 24329 -> 23716 (−613 Б), свободно в резиденте 129 ->
747 Б. make size-check чистый (67 программ), звук в MAME проверен —
насос отработал 696 запросов, pop_snd_ok/want = 1/1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Две разные болячки, обе разобраны записью звука MAME в WAV.
libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает
воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то,
что лежало раньше. Своими данными звук идёт лишь с третьей половины:
прерывание приходит на 128-м слоте и ставит указатель на противоположную
половину. _cbl_prime заливает буфер тишиной сразу после включения (раньше
нельзя — запись проходит только при поднятом bit7). В MAME это немо
(эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе
это ровно тот мусор, что ловился на тестовых примерах CBL.
PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один
пропущенный блок (128 сэмплов). Причина: pop_shutdown освобождал атласы и
графику через ESTEX при открытом звуке, насос не успевал долить. Звук
гасим первым действием. Проверено записью — всплеска больше нет.
Заодно записан разбор стартового всплеска (sound_plan.md §10): это не
мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land. Обрыв
одноголосьем — поведение оригинала (play_digi_sound начинается с
stop_digi, seg009.c:2402).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание
на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе
это уже не так.
gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная
позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7
порта 0x004E. Разделяемое владение портом с CBL через
_cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) —
cbl_close() возвращает "немой" режим вместо полного выключения, если
gfx ещё держит ссылку. Fallback на halt при таймауте.
sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо
подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду —
не переполняет uint16_t и не требует умножения/32-бит арифметики),
общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на
старое поведение при EBUSY (фрейм-хук занят другим irq_install()).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.
- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
tests/cblstream: единственный случай с собственным кольцом уровня
приложения (диск нельзя читать из fill()).
Verified в MAME 2026-07-07 — все три теста работают.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>