libc/cbl: выключение CBL больше не оставляет железо петь одну ноту

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
This commit is contained in:
2026-09-02 10:55:15 +03:00
parent 63bfd997a9
commit b34997073e
5 changed files with 81 additions and 19 deletions
+31 -9
View File
@@ -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(), при недоливе — тишина по режиму (один