Files
Sprinter-SDCC/libc/cbl/_cbl_prime.c
T
snark13 b34997073e 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
2026-09-02 10:55:15 +03:00

69 lines
5.1 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после записи в
* порт управления: и при включении, и при выключении.
*
* ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни
* при сбросе, ни при записи в порт управления — там остаётся то, что лежало
* раньше: хвост прошлой сессии, а на холодном старте вообще неинициализи-
* рованное содержимое. Между тем запись в порт управления взводит счётчик
* воспроизведения в 0 и тут же пускает таймер, поэтому первое, что уходит в
* ЦАП, — эти самые 256 слотов. На слух это кусок мусора в момент включения
* звука (поймано пользователем на старте PoP, 2026-08-20).
*
* Разбор по MAME (src/mame/sinclair/sprinter.cpp, единственная доступная нам
* модель железа):
* - case 0x89 (порт управления): `m_cbl_cnt = 0; m_cbl_wa = 0;` и завод
* таймера — буфер при этом не трогается;
* - cbl_tick: играет `m_cbl_data[m_cbl_cnt++]`, а прерывание «долей
* половину» поднимает только на `!(m_cbl_cnt & 0x7f)`, ставя указатель
* записи на ПРОТИВОПОЛОЖНУЮ половину (`m_cbl_wa = m_cbl_cnt ^ 0x80`).
* Отсюда ровно: первое прерывание приходит, когда сыграна половина 0..127,
* и приложение заполняет её же, пока играет половина 128..255. Своими
* данными звук пойдёт только с третьей половины, то есть НЕЗАПОЛНЕННЫМИ
* уходят все 256 слотов — 23,4 мс на 10 937,5 Гц.
*
* ПОЧЕМУ ИМЕННО ТАК ЛЕЧИТСЯ. Заранее, до включения, залить нельзя: запись
* в порт данных попадает в буфер только при уже поднятом bit7 (`case 0x88:
* if (cbl_mode())`). Значит заливаем сразу ПОСЛЕ включения — за 256 OUT'ов
* (~6 400 тактов, 0,3 мс) таймер успевает продвинуться на два-три слота, и
* наружу проскакивает пара сэмплов вместо 23 мс. Гонки с насосом нет:
* первое прерывание будет только на 128-м слоте, а зовут нас под DI.
*
* Слотов ровно 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"
void _cbl_prime(uint8_t silence) __naked
{
(void)silence;
__asm
;; __sdcccall(1): uint8_t-аргумент уже в A.
;; C = порт данных, B = счётчик (B=0 значит 256 итераций). Пишем
;; через `out (c),a`, а не `out (n),a`: у OTIR в этот порт старший
;; байт адреса тоже задаёт B и меняется по ходу повторяем ровно
;; ту же картину на шине, чтобы не зависеть от декодирования.
ld c, #_CBL_DATA_PORT
ld b, #0
00001$:
out (c), a
djnz 00001$
ret
__endasm;
}