Files
Sprinter-SDCC/applications/SprPoP/src/pop_sfx_cold.c
T
snark13 7377bf6a2c SprPoP: заставка и музыка сразу после запуска, а не через несколько секунд
От запуска до проявления титула проходило ~3 с чёрного экрана в тишине:
pop_boot читал ВСЕ ресурсы игры до первого пикселя, а pop_title_show перед
fade_in вычитывал целиком трек заставки (248 КБ).

Старт разбит на три фазы (sprpop_cold.c):
- pop_boot() — настройки, POP.CFG, графика, чёрный экран, FONT.ATL;
- pop_boot_sound() — snd.idx + ОДНА страница snd.arc, индекс музыки, CBL;
- pop_boot_rest() — остальные страницы звука, kid.ani, Тень, атласы Кида;
  идемпотентно, между кусками доливает трек.

pop_title_show теперь: fade_in -> pop_boot_sound -> pop_music_stream(54) ->
pop_boot_rest под стоящим титулом.  Порядок «сначала fade_in, потом тема»
сохранён как у show_title (seg000.c:1981).  Первый такт заставки укорочен
на фактически потраченное время (часы насоса 85,4 Гц -> кадры луча
сдвигами), иначе сцена уехала бы относительно музыки.  title_wait доливает
трек по полстраницы за кадр.

Музыка играет с ПЕРВОЙ страницы: pop_music_stream уже был (игровые
джинглы), кольцо won не понадобилось — m54 это 16 страниц из 20 доступных.

Звук поднимается в два приёма (pop_sfx_init_begin/finish): насосу для
тишины нужен ровно один блок, и упаковщик обязан класть его первым блоком
страницы 0 (tools/pop_idx.py) — значит для открытия CBL хватает ОДНОЙ
страницы (33 мс вместо 440).  Пока набор неполон, pop_sfx_play пропускает
эффекты; заявки на музыку проходят.

Четыре загрузки из boot УБРАНЫ, а не отложены: атлас фона, страж, уровень 1
и pop_pal_game_load — их и так делает pop_level_switch на входе в любой
уровень (а pop_new_game_load ещё и палитру перед ним), и оба маршрута в
игру, LEVEL_LOAD и DEMO, упираются в него.  Это ~0,77 с на ровном месте.
Так же устроен оригинал: init_game_main до заставки читает только меч,
пламя, звуки и палитры, а chtab_5/6/7 грузит load_lev_spr на старте уровня.
Страховка на случай пропуска заставки в первую секунду — идемпотентный
pop_boot_rest() в начале pop_new_game_load и pop_level_switch.

FONT.ATL остаётся в минимальной фазе, и это не про шрифт: снимок палитры
физически лежит в хвосте его страницы (pop_ui.c), а вся машинерия яркости
начинается с `if (!font_ready) return`.  С отложенным шрифтом заставка
возникала разом на полной яркости — поймано пользователем.

Проверено в MAME: титул виден сразу и проявляется полосами, pop_mus_left
убывает (трек стримится), pop_snd_pages=10 (набор дочитался), цепочка
титул -> интро -> пропуск -> уровень 1 работает.  Банк 8: 95,5 % (732 Б).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:30:53 +03:00

304 lines
16 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.
/*
* pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
*/
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include <cbl.h>
#include "pop_sfx.h"
#include "pop_arc.h"
#include "_pop_sfx.h"
#include "_pop_file.h" /* POP_PATH_CALL — старые DSS и пути */
#include <irq.h> /* IRQ_DISABLE вокруг записи в W0 */
#include <sprite.h> /* gfx_w0_map — залить блок тишины */
static uint8_t snd_blk; /* блок EMM под весь набор */
/* Раздельная загрузка (см. шапку pop_sfx.h): сколько страниц обещал индекс
* и сколько уже прочитано. 0 в want — begin не проходил либо поднялась
* только страница тишины (набора нет), и дочитывать нечего. */
static uint8_t snd_want_pages;
static uint8_t snd_done_pages;
/* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
*
* Раскладка эффектов приезжает с диска, а не из EXE: так пара
* snd.idx + snd.arc заменяется целиком (SDLPoP <-> MSDOS) без
* перекомпиляции игры. Писатель формата — tools/pop_idx.py, он же
* покрыт тестом; проверки ниже повторяют его один в один, потому что
* доверять содержимому файла нельзя: битый индекс увёл бы насос за
* пределы выделенного блока EMM.
*
* Заголовок: 16 байт, дальше POP_SND_COUNT записей по 5 байт подряд —
* ровно в том виде, в каком лежит pop_snd_tbl[], поэтому читаются они
* ОДНИМ read'ом прямо туда, без разбора по полям. */
#define IDX_HDR_SIZE 16
#define IDX_BLOCK_LOG2 7 /* 1 << 7 == POP_SND_BLOCK */
#define IDX_PCM_FMT 1 /* u8 mono, CBL_FREQ_10K9 */
#define IDX_TBL_SIZE (POP_SND_COUNT * (int)sizeof(pop_snd_ent_t))
/* Имя знает сам модуль — литерал лежит в rodata банка 8, и open() читает
* его, пока в W3 стоит этот же банк (тот же приём, что в pop_arc.c). */
static const char idx_path[] = "SND\\snd.idx";
/* Одна запись: ровно те проверки, что делает упаковщик.
*
* СЧИТАЕМ В БЛОКАХ, А НЕ В БАЙТАХ. Байтовый адрес конца эффекта
* (page * 16384 + off + len) не влезает в uint16, а 32-битная арифметика
* на Z80 дорога (memory avoid_32bit_arith_z80). В блоках по 128 весь
* набор — не больше POP_SND_MAX_PAGES * 128 = 2048, и всё считается
* шестнадцатью битами. */
static int8_t idx_entry_ok(const pop_snd_ent_t *e, uint8_t pages)
{
uint16_t start, end;
if (e->len == 0) /* нет оцифровки — это музыка */
return (e->page == 0 && e->off == 0) ? 0 : -1;
if (e->page >= pages) return -1;
if (e->off >= 0x4000) return -1;
if ((e->off | e->len) & (POP_SND_BLOCK - 1)) return -1;
/* Блок тишины неприкосновенен: насос льёт его, когда играть нечего. */
if (e->page == POP_SND_SILENCE_PAGE && e->off < POP_SND_BLOCK) return -1;
start = (uint16_t)((uint16_t)e->page << 7) + (uint16_t)(e->off >> 7);
end = (uint16_t)(start + (uint16_t)(e->len >> 7));
/* Длинный эффект переливается на следующие страницы — они обязаны
* быть загружены, иначе насос уйдёт в чужую память. */
if (end > (uint16_t)((uint16_t)pages << 7)) return -1;
return 0;
}
/* Прочитать и проверить индекс. Возвращает число страниц набора либо 0. */
static uint8_t idx_load(void)
{
uint8_t head[IDX_HDR_SIZE];
uint8_t pages, i;
int fd;
POP_PATH_CALL(fd, open(idx_path, O_RDONLY));
if (fd < 0) return 0;
if (read(fd, head, IDX_HDR_SIZE) != IDX_HDR_SIZE ||
memcmp(head, "PSI1", 4) != 0 ||
head[4] != POP_SND_COUNT ||
head[5] != (uint8_t)sizeof(pop_snd_ent_t) ||
head[6] != IDX_BLOCK_LOG2 ||
head[7] != IDX_PCM_FMT) {
close(fd);
return 0;
}
pages = head[8];
if (pages == 0 || pages > POP_SND_MAX_PAGES) { close(fd); return 0; }
/* Записи ложатся прямо в резидентную таблицу: дисковый формат и
* структура совпадают побайтово (проверено статически в
* pop_snd_data.c и полем head[5] выше). */
if (read(fd, pop_snd_tbl, IDX_TBL_SIZE) != IDX_TBL_SIZE) {
close(fd);
return 0;
}
close(fd);
for (i = 0; i < POP_SND_COUNT; i++)
if (idx_entry_ok(&pop_snd_tbl[i], pages) != 0) return 0;
return pages;
}
/* НАБОРА НЕТ — НО НАСОС ВСЁ РАВНО ОБЯЗАН РАБОТАТЬ.
*
* Тишину в паузах насос льёт из ПЕРВОГО БЛОКА НАБОРА, поэтому без набора
* CBL не открывался вовсе. А вместе с ним вставала и МУЗЫКА: её блоки
* отсчитывает тот же насос (pop_mus_left убывает только в pop_sfx_fill).
* Заставка при этом висла намертво — ждала конца трека, который не
* начинал играть, и pop_music_busy() не гас никогда (поймано
* пользователем 2026-08-31 на образе без snd.idx).
*
* Поэтому при отказе индекса поднимаем ПУСТОЙ набор: одна страница EMM с
* блоком тишины. Эффектов нет — все len в таблице нулевые, и
* pop_sfx_play уходит по ветке «нет оцифровки», — а музыка играет.
*
* ЗАЛИВАЕМ ТОЛЬКО ПЕРВЫЙ БЛОК, и это не экономия: gfx_w0_page_prepare
* ставит в страницу IRQ/NMI-стабы, чтобы прерывание при замапленном W0
* нашло валидный код. Заливка ВСЕЙ страницы затирает их, и первое же
* прерывание уводит процессор в мусор — чёрный экран на старте (поймано
* при первой попытке этого фикса). Насосу больше 128 байт и не нужно:
* он берёт ровно блок тишины, да и то через W3, а не через W0.
* Запись — под DI: 128 байт накрывают стаб RST38, и до unmap прерываний
* быть не должно. */
static int8_t snd_silence_only(void)
{
uint8_t blk, page;
uint8_t *w0 = (uint8_t *)0x0000; /* окно 0 — сюда ляжет страница */
memset(pop_snd_tbl, 0, sizeof(pop_snd_tbl));
blk = mem_alloc_pages(1);
if (!blk) return -1;
page = mem_get_page(blk, 0);
if (!page) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
IRQ_DISABLE();
gfx_w0_map(page);
memset(w0, 0x80, POP_SND_BLOCK); /* 0x80 — тишина в 8-бит без знака */
gfx_w0_unmap();
IRQ_ENABLE();
pop_snd_page[POP_SND_SILENCE_PAGE] = page;
snd_blk = blk;
pop_snd_pages = 1;
return 0;
}
/* ПЕРВАЯ ПОЛОВИНА: индекс + страница 0 (в ней блок тишины). После неё
* звук можно открывать, и музыка играет; эффекты — после finish. */
int pop_sfx_init_begin(void) __banked
{
uint8_t blk, pages;
if (snd_blk) return 0; /* набор уже поднимали */
/* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой
* эффект лёг — знает только он. Набора без индекса не бывает:
* держать в EXE запасную таблицу нельзя, иначе привязка бинарника к
* конкретному набору вернётся через чёрный ход. */
pages = idx_load();
if (!pages) {
/* Набор эффектов не поднялся — но звук как таковой поднять надо,
* иначе встанет музыка (см. snd_silence_only). Возвращаем -1:
* эффектов действительно нет, и вызывающий вправе об этом знать. */
if (snd_silence_only() != 0) return -1;
pop_snd_want = 1;
return -1;
}
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
* таблица звуков адресует их именно так (page = смещение >> 14).
*
* Из АРХИВА, а не десятью файлами: `open` стоит 51,4 мс против 32,6 мс
* на чтение 16 КБ (замер 2026-08-25), то есть девять лишних открытий —
* это почти полсекунды на ровном месте. Таблица живёт на стеке, см.
* pop_arc.h. */
{
pop_arc_t arc;
uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC];
/* Архив обязан содержать РОВНО столько страниц, сколько обещал
* индекс: расходящейся паре доверять нельзя — она означает, что
* snd.idx и snd.arc из разных наборов. */
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
(int8_t)pages)
return -1;
/* Блок выделяем СРАЗУ на весь набор: номера страниц отдаёт
* mem_get_page по индексу, и finish дочитает в те же самые. */
blk = mem_alloc_pages(pages);
if (!blk) { pop_arc_close(&arc); return -1; }
pop_snd_page[POP_SND_SILENCE_PAGE] = mem_get_page(blk, 0);
if (!pop_snd_page[POP_SND_SILENCE_PAGE] ||
pop_arc_read(&arc, tbl, 0, 0,
pop_snd_page[POP_SND_SILENCE_PAGE], 0, 0) != 16384) {
pop_arc_close(&arc);
mem_free_block(blk);
return -1;
}
pop_arc_close(&arc);
}
snd_blk = blk;
snd_want_pages = pages;
snd_done_pages = 1;
/* pop_snd_pages НЕ ставим: набор ещё неполон, и до finish эффекты
* запускать нельзя (страницы выделены, но не прочитаны). */
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
return 0;
}
/* ВТОРАЯ ПОЛОВИНА: дочитать остальные страницы и разрешить эффекты.
* Идемпотентна; архив открывается второй раз (51 мс) — это дешевле, чем
* держать открытый манипулятор через всю заставку при лимите в восемь. */
int pop_sfx_init_finish(void) __banked
{
pop_arc_t arc;
uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC];
uint8_t i;
if (pop_snd_pages) return 0; /* набор уже полный */
if (!snd_want_pages) return -1; /* begin не проходил либо только тишина */
if (snd_done_pages < snd_want_pages) {
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
(int8_t)snd_want_pages)
return -1;
for (i = snd_done_pages; i < snd_want_pages; i++) {
pop_snd_page[i] = mem_get_page(snd_blk, i);
if (!pop_snd_page[i] ||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
pop_arc_close(&arc);
return -1; /* страницы не отдаём: тишина в стр.0 жива */
}
snd_done_pages = (uint8_t)(i + 1);
}
pop_arc_close(&arc);
}
/* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан
* видеть 0 и не пытаться ходить по полузаполненной таблице. */
pop_snd_pages = snd_want_pages;
return 0;
}
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
*
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
* диск надолго, насос не успевает долить очередные 128 байт, и железо
* крутит по кругу хвост своего 256-байтового буфера. На слух это
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
* включаем звук последним действием, а на время загрузки уровня глушим
* (pop_sfx_pause).
*
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
* cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
* набора), и просить у libc буфер через malloc не надо — куча в резиденте
* W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
int pop_sfx_start(void) __banked
{
if (pop_snd_ok) return 0;
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
pop_sfx_fill) != 0)
return -1;
pop_snd_ok = 1;
return 0;
}
void pop_sfx_pause(void) __banked
{
if (!pop_snd_ok) return;
pop_sfx_stop();
cbl_close();
pop_snd_ok = 0;
}
void pop_sfx_close(void) __banked
{
pop_sfx_pause();
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
}
/* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP
* (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл:
* иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это
* 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание
* при периоде 245 759). Открытие обратно дешёвое — cbl_open только
* настраивает железо, страницы набора никуда не девались. */
void pop_sfx_toggle(void) __banked
{
if (pop_snd_want) {
pop_sfx_pause(); /* pause сам не трогает want... */
pop_snd_want = 0; /* ...поэтому гасим его следом */
} else {
pop_snd_want = 1;
/* Набора может не быть вовсе (файлы не прочитались, страницы не
* дались) — тогда откатываем флаг, чтобы индикация не врала. */
if (pop_sfx_start() != 0) pop_snd_want = 0;
}
}