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
+4 -4
View File
@@ -15,9 +15,9 @@ bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 5860 cblstream 5862
cbltest 6006 cbltest 6008
cblwav 6099 cblwav 6101
conio 4605 conio 4605
conio2 3929 conio2 3929
convbench 3508 convbench 3508
@@ -28,7 +28,7 @@ fdmax 6023
filetest 10547 filetest 10547
fpsdiv 4870 fpsdiv 4870
gets 509 gets 509
gfx_dbuf 5131 gfx_dbuf 5142
gfx_demo 4114 gfx_demo 4114
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
15 blitperf
16 blitw
17 cat
18 cblstream
19 cbltest
20 cblwav
21 conio
22 conio2
23 convbench
28 filetest
29 fpsdiv
30 gets
31 gfx_dbuf
32 gfx_demo
33 gfxbanks
34 hello
+31 -9
View File
@@ -26,12 +26,29 @@
* *
* _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих * _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих
* держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL- * держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL-
* сессии. Нужно gfx_wait_vsync() (см. libc/gfx/gfx_wait_vsync.c): бит * сессии. Нужно gfx_wait_vsync() (см. libbgi/common/gfx_wait_vsync.c):
* 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp kbd_fe_r), * бит 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp
* но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный CBL уже * kbd_fe_r), но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный
* открыт — портом владеет он, ref/unref его не трогают; иначе включают * CBL уже открыт — портом владеет он, ref/unref его не трогают; иначе
* bit7 "немым" кодом частоты _CBL_VSYNC_FREQ (таймер CBL не заводится * включают bit7 "немым" кодом частоты _CBL_VSYNC_FREQ и заливают
* — divs[2]=0 в MAME — ни звука, ни прерываний, только синхро-бит). * аппаратный буфер тишиной — БЕЗ ЗАЛИВКИ ЖЕЛЕЗО ПОЁТ (см. ниже).
*
* ЖЕЛЕЗО ПОЁТ ТО, ЧТО ЛЕЖИТ В БУФЕРЕ. Буфер 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 #ifndef _CBL_INTERNAL_H
#define _CBL_INTERNAL_H #define _CBL_INTERNAL_H
@@ -42,7 +59,10 @@
#define _CBL_CTRL_PORT 0x004E #define _CBL_CTRL_PORT 0x004E
#define _CBL_DATA_PORT 0x4F #define _CBL_DATA_PORT 0x4F
#define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */ #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_block; /* размер блока: 128 или 256 (по формату) */
extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */ 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); uint8_t underrun_mode, cbl_fill_fn fill);
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после /* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше. * ЛЮБОЙ записи в порт управления: при включении — иначе первые 23 мс
* Подробный разбор — в шапке _cbl_prime.c. */ * играет то, что лежало в буфере раньше; при выключении — иначе железо
* крутит хвост последнего сэмпла бесконечной нотой. Подробный разбор —
* в шапке _cbl_prime.c. */
void _cbl_prime(uint8_t silence); void _cbl_prime(uint8_t silence);
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один /* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
+20 -3
View File
@@ -7,9 +7,23 @@
* *
* Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет * Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет
* он, ref/unref его не трогают вообще (это уже нужное состояние). * он, ref/unref его не трогают вообще (это уже нужное состояние).
* Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ * Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ.
* — таймер CBL при этом коде не заводится (divs[2]=0 в MAME), значит *
* ни звука, ни прерываний, только доступ к синхро-биту. * ТИШИНУ ДАЁТ ЗАЛИТЫЙ БУФЕР, А НЕ ОСТАНОВЛЕННЫЙ ТАЙМЕР. Раньше здесь
* стоял 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" #include "_cbl.h"
@@ -19,6 +33,9 @@ void _cbl_port_sync(void)
{ {
if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */ if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */
_cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0); _cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0);
/* Формат "немого" режима всегда 8-битный (bit5 не выставлен), поэтому
* тишина здесь — 0x80 независимо от того, каким был закрытый сеанс. */
_cbl_prime(0x80);
} }
void _cbl_port_ref(void) void _cbl_port_ref(void)
+15 -1
View File
@@ -1,5 +1,6 @@
/* /*
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после включения. * _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после записи в
* порт управления: и при включении, и при выключении.
* *
* ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни * ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни
* при сбросе, ни при записи в порт управления — там остаётся то, что лежало * при сбросе, ни при записи в порт управления — там остаётся то, что лежало
@@ -31,6 +32,19 @@
* Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый * Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый
* сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для * сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для
* 8-бит беззнакового, 0x00 для 16-бит. * 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" #include "_cbl.h"
+11 -2
View File
@@ -10,7 +10,16 @@
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync() * Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит * пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
* ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита * ссылку (_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" #include "_cbl.h"
@@ -21,7 +30,7 @@ void cbl_close(void)
if (!_irq_cbl_hook) return; if (!_irq_cbl_hook) return;
IRQ_DISABLE(); IRQ_DISABLE();
_irq_cbl_hook = 0; _irq_cbl_hook = 0;
_cbl_port_sync(); /* режим порта + заливка буфера тишиной */
IRQ_ENABLE(); IRQ_ENABLE();
_cbl_port_sync();
_irq_table_unref(); _irq_table_unref();
} }