7377bf6a2c
От запуска до проявления титула проходило ~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
304 lines
16 KiB
C
304 lines
16 KiB
C
/*
|
||
* 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;
|
||
}
|
||
}
|