b34997073e
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
105 lines
7.7 KiB
C
105 lines
7.7 KiB
C
/*
|
||
* _cbl.h — внутренности CBL-модуля (НЕ публичный заголовок).
|
||
*
|
||
* Механика (форум Sprinter Team + официальная документация "5.3
|
||
* COVOX-Blaster" + docs/converted/accel_r.txt):
|
||
* - порт управления 0x004E — 16-битный (только ld bc,#0x004E /
|
||
* out (c),a): bit7 = CBL on, bit6 = stereo, bit5 = 16-бит,
|
||
* bit4 = int enable, bits3..0 = частота; выключение — запись 0;
|
||
* - буфер 256 Б аппаратно разбит на две половины по 128 Б; бит 7
|
||
* порта 0xFE отражает, какая половина ИГРАЕТ сейчас — флип этого
|
||
* бита поднимает прерывание (вектор 0xFF), пора долить неактивную
|
||
* половину;
|
||
* - размер блока — 128 Б для 8-бит формата, 256 Б для 16-бит (не
|
||
* зависит от моно/стерео) — см. _cbl_block;
|
||
* - выдача блока — ДВА способа (cbl_push_otir.c/cbl_push_accel.c):
|
||
* OTIR в порт 0x4F, либо акселератором в спец-страницу EMM 0xFD
|
||
* (маппится в W3 на 0xC000); библиотека сама НИЧЕГО не пропихивает
|
||
* — это делает fill()-callback приложения;
|
||
* - насос вызывается из кадрового трамплина через хук _irq_cbl_hook
|
||
* (см. _irq.h) — прямой ссылки irq→cbl нет, DCE сохраняется.
|
||
*
|
||
* Никакого собственного кольца НЕТ — второй буфер поверх аппаратного
|
||
* был бы избыточен. _cbl_fill — callback приложения; _cbl_silence —
|
||
* буфер тишины, malloc'ится в cbl_open() ТОЛЬКО при
|
||
* CBL_UNDERRUN_SILENCE (иначе NULL).
|
||
*
|
||
* _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих
|
||
* держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL-
|
||
* сессии. Нужно 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
|
||
|
||
#include <stdint.h>
|
||
#include <cbl.h>
|
||
|
||
#define _CBL_CTRL_PORT 0x004E
|
||
#define _CBL_DATA_PORT 0x4F
|
||
#define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */
|
||
#define _CBL_VSYNC_FREQ 0x08 /* "немой" режим: самый медленный ДОКУМЕНТИРОВАННЫЙ
|
||
* код (7,8125 кГц). Тишина обеспечена не
|
||
* остановкой таймера (это свойство MAME, не
|
||
* железа), а залитым тишиной буфером. */
|
||
|
||
extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */
|
||
extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */
|
||
extern uint16_t _cbl_undr; /* из них недолив (fill вернул 0/не задан) */
|
||
extern cbl_fill_fn _cbl_fill; /* callback приложения, может быть NULL */
|
||
extern uint8_t _cbl_underrun_mode; /* CBL_UNDERRUN_* */
|
||
extern uint8_t *_cbl_silence; /* буфер тишины или NULL */
|
||
extern uint8_t _cbl_port_refs; /* держатели bit7 помимо реальной CBL-сессии */
|
||
|
||
/* Запись в 16-битный порт управления 0x004E. */
|
||
void _cbl_ctrl(uint8_t v);
|
||
|
||
/* Общее тело обеих публичных точек входа (cbl_open / cbl_open_silence).
|
||
* Буфер тишины не трогает — им владеет cbl_open_silence.c; так malloc не
|
||
* приезжает в резидент приложениям, которые льют тишину сами. */
|
||
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. */
|
||
void _cbl_prime(uint8_t silence);
|
||
|
||
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
|
||
* регистрируется в _irq_cbl_hook при cbl_open — по выбору CBL_PUMP_*). */
|
||
void _cbl_pump_otir(void);
|
||
void _cbl_pump_accel(void);
|
||
|
||
/* Разделяемое владение портом 0x004E (см. описание выше).
|
||
* _cbl_port_sync пересчитывает нужное значение порта из текущего
|
||
* состояния (реальная сессия? держатели?) и пишет его — вызывается
|
||
* из ref/unref, а также из cbl_close() после снятия _irq_cbl_hook. */
|
||
void _cbl_port_ref(void);
|
||
void _cbl_port_unref(void);
|
||
void _cbl_port_sync(void);
|
||
|
||
#endif
|