SprPoP: звуковые эффекты без перелинковки — раскладка уехала на диск
Часть II плана music_runtime_index_plan.md (SI0..SI4). gen/pop_sound_tbl.h БОЛЬШЕ НЕ ГЕНЕРИРУЕТСЯ: раскладка набора читается из SND/snd.idx (формат PSI1, писатель и разборщик — tools/pop_idx.py, 22 теста в make test-tools). Один и тот же sprpop.exe работает с набором SDLPoP (9 страниц) и MSDOS (10) — sha256 бинарника при смене набора не меняется. Заодно умолчание источника эффектов переведено на SDLPoP (SND_SRC=sdlpop): сборка обязана работать без оригинального дистрибутива DOS. У кого он есть, включает лучший набор явно — make SND_SRC=msdos (там полнее оцифровка: в SDLPoP звук 48 spiked пустой). Устройство: pop_snd_tbl/pop_snd_page/pop_snd_pages — резидентные данные (pop_snd_data.c), тип и инварианты — рукописный pop_snd_tbl.h. Записи читаются ОДНИМ read прямо в таблицу, поэтому sizeof(pop_snd_ent_t) == 5 стало частью дискового контракта: проверяется статически и полем размера записи в заголовке. POP_SND_PAGES как compile-time размер набора исчез — вместо него POP_SND_MAX_PAGES (вместимость, 16) и runtime pop_snd_pages. Цена: таблица переехала из _CODE в _DATA, суммарный резидент почти не изменился (куча 239 -> 229 Б); банк 8 +601 Б на чтение и валидацию. Валидация не доверяет файлу: заголовок целиком плюс каждая запись (страница, смещение, кратность блоку, непересечение с блоком тишины, выход за последнюю страницу). Последнее считается В БЛОКАХ — байтовый адрес конца не влезает в uint16, а 32-битная арифметика на Z80 дорога. НЕТ ИНДЕКСА — ЭФФЕКТОВ НЕТ, НО МУЗЫКА ИГРАЕТ. Первая версия просто возвращала ошибку, и игра становилась непроходимой: тишину льёт первый блок набора, без набора CBL не открывался, а с ним вставала музыка (её блоки считает тот же насос) — заставка ждала конца трека вечно. Теперь поднимается пустой набор с блоком тишины. Заливается ровно 128 байт и под DI: gfx_w0_page_prepare ставит в страницу IRQ-стабы, и заливка всей страницы затирала их — первое же прерывание давало чёрный экран. Грабли сборки: смена SND_SRC тихо давала неверный результат (sdlpop -> msdos -> sdlpop оставлял чужой набор в assets/packed). Причина не в логике, а в секундной гранулярности mtime. Лечение убирает время из решения: смена варианта сносит stamp'ы своего семейства, а упаковка, сборка архива и копия индекса делаются одним рецептом. То же получила и музыка (MUSIC_FMT). Проверено в MAME: таблица в памяти совпадает с файлом из образа побайтово; один EXE поднимает оба набора; отладочный --order reverse (30 из 31 записей отличаются от штатных) звучит правильно; битый индекс выключает эффекты, не роняя игру; без индекса PV-сцена проходит с музыкой; Ctrl+S работает в обоих режимах. Разбор — docs/sound_plan.md §10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
||||
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
||||
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
||||
| [`../assets/orig/README.md`](../assets/orig/README.md) | **Откуда берутся исходные данные**: `make fetch` (SDLPoP + музыка), зачем нужен и почему необязателен `MSDOS/`, `SND_SRC` |
|
||||
|
||||
## Производительность
|
||||
|
||||
|
||||
@@ -133,3 +133,51 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
|
||||
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||||
|
||||
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||||
|
||||
## Музыка одним постоянно открытым архивом (замер 2026-08-31, НЕ сейчас)
|
||||
|
||||
**Идея.** Сейчас каждый трек — отдельный файл `MUS/mNN.bin`, и
|
||||
`pop_music_load_begin` открывает свой на каждый запуск. Свести треки в
|
||||
один файл и держать его `fd` открытым: вместо `chdir`+`open`+`chdir`
|
||||
останется `lseek` к смещению трека.
|
||||
|
||||
**Почему это стоит внимания — разложение окна старта музыки** (полный
|
||||
замер и метод — `sound_plan.md` §9):
|
||||
|
||||
| шаг | цена | уйдёт? |
|
||||
|---|---:|---|
|
||||
| ожидание `pop_music_service` в кадре | 22,6 мс | нет |
|
||||
| `chdir` #1 | 46,1 мс | **да** |
|
||||
| `open` файла трека | 34,5 мс | **да** |
|
||||
| `chdir` #2 | 59,0 мс | **да** |
|
||||
| чтение первых 8 КБ | 34,0 мс | нет |
|
||||
| **итого** | **196,7 мс** | |
|
||||
|
||||
Уходит **139,6 мс** — но это ВЕРХНЯЯ граница: появляется `lseek` к
|
||||
смещению трека, которого сейчас нет вовсе (файл читается последовательно
|
||||
с нуля), и его цена НЕ ИЗМЕРЕНА. Реальный выигрыш = 139,6 минус `lseek`;
|
||||
померить можно тем же способом, `lseek` уже используется в
|
||||
`pop_arc.c:89`. Остаётся ≈ 57 мс, то есть старт трека ускоряется
|
||||
примерно вчетверо.
|
||||
|
||||
**Где заметно.** Реплики PV-сцены идут встык (три трека подряд), плюс
|
||||
каждый игровой джингл — смерть, зелье, подобранный меч.
|
||||
|
||||
**Чем осложнено.**
|
||||
|
||||
1. **`PBA1` под это не годится.** Размер элемента там `uint16` (≤ 64 КБ),
|
||||
а трек — до 1,2 МБ (m56, 78 страниц); резать по страницам нельзя,
|
||||
элементов вышло бы ~240 при `POP_ARC_MAX = 126`. Нужен свой индекс со
|
||||
смещениями — то есть эта задача СМЫКАЕТСЯ с `MUS/MUSIC.IDX` из
|
||||
`music_runtime_index_plan.md`: одно изменение раскладки, а не два.
|
||||
2. **Постоянно занятый файловый манипулятор** — один из восьми (memory
|
||||
`dss_fd_limit`, девятый `open` вешает DSS). Рядом свои открывают
|
||||
`POP.CFG`, quicksave и загрузка уровня.
|
||||
3. **`chdir` убрать нельзя** — он и есть половина выигрыша, но нужен
|
||||
старым DSS (`POP_PATH_CALL`); экономия берётся не его удалением, а тем,
|
||||
что открытие вообще перестаёт выполняться на каждый трек.
|
||||
|
||||
**Оговорка.** Задержка старта музыки сама по себе НЕ является дефектом:
|
||||
трек начинается на границе события, на слух это не сбой. У эффектов
|
||||
такой задержки нет вовсе — они целиком в EMM (см. `sound_plan.md` §9.1).
|
||||
Так что это оптимизация, а не исправление.
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||||
|
||||
> Статус: план будущего улучшения, 2026-08-30. Код пока не менялся.
|
||||
> Статус: **часть II (эффекты, SI0..SI4) СДЕЛАНА 2026-08-31** — разбор и
|
||||
> результаты в `sound_plan.md` §10. Часть I (музыка, MI0..MI5) — план.
|
||||
>
|
||||
> Что в реализации разошлось с планом ниже: файл назван `SND/snd.idx`
|
||||
> (единый basename с `snd.arc`, оба в нижнем регистре), а при отсутствии
|
||||
> индекса поднимается ПУСТОЙ набор с блоком тишины — иначе встаёт музыка
|
||||
> и игра непроходима (§10.5 sound_plan). Про `ticks60` в `PMI1` решено:
|
||||
> поле в формате оставить даже если сценарии перейдут на ожидание звука —
|
||||
> загрузчик волен не тащить его в память.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
@@ -330,13 +338,13 @@ Makefile должен:
|
||||
|
||||
Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов,
|
||||
если в них сохранена исходная нумерация sound id 0..56. Замена набора должна
|
||||
сводиться к замене согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC`, без
|
||||
сводиться к замене согласованной пары `SND/snd.idx` + `SND/snd.arc`, без
|
||||
генерации C-заголовка и перелинковки EXE.
|
||||
|
||||
Набор состоит из двух согласованных файлов:
|
||||
|
||||
- `SND/SND.ARC` — обычный архив PBA1 с PCM-страницами;
|
||||
- `SND/SOUND.IDX` — описание раскладки эффектов внутри этих страниц.
|
||||
- `SND/snd.arc` — обычный архив PBA1 с PCM-страницами;
|
||||
- `SND/snd.idx` — описание раскладки эффектов внутри этих страниц.
|
||||
|
||||
Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся
|
||||
универсальным и не получает специального варианта только для звука, а
|
||||
@@ -381,7 +389,7 @@ typedef struct {
|
||||
У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого
|
||||
набора, а не ошибка runtime-индекса.
|
||||
|
||||
## 15. Файл `SND/SOUND.IDX`
|
||||
## 15. Файл `SND/snd.idx`
|
||||
|
||||
IDX занимает один 512-байтовый сектор и после короткого заголовка является
|
||||
точным дисковым дампом 57 записей `pop_snd_tbl`:
|
||||
@@ -472,12 +480,12 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
||||
|
||||
`pop_sfx_init()` должен выполнять следующую последовательность:
|
||||
|
||||
1. открыть `SND/SOUND.IDX` через существующий путь `POP_PATH_CALL`;
|
||||
1. открыть `SND/snd.idx` через существующий путь `POP_PATH_CALL`;
|
||||
2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
|
||||
`pop_snd_tbl`, и проверить полный размер IDX;
|
||||
3. проверить magic, версию, число/размер записей, формат и размер блока;
|
||||
4. получить из IDX фактическое число страниц и проверить диапазон 1..16;
|
||||
5. открыть `SND/SND.ARC` через `POP_PATH_CALL`;
|
||||
5. открыть `SND/snd.arc` через `POP_PATH_CALL`;
|
||||
6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`;
|
||||
7. сверить `PSI1.pages` с числом элементов PBA1;
|
||||
8. проверить каждую ненулевую запись;
|
||||
@@ -493,7 +501,7 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
||||
`pop_snd_pages`.
|
||||
|
||||
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
|
||||
используется, эффекты остаются выключенными. Набор без `SOUND.IDX` считается
|
||||
используется, эффекты остаются выключенными. Набор без `snd.idx` считается
|
||||
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
|
||||
зависимость бинарника от конкретного набора останется.
|
||||
|
||||
@@ -505,9 +513,9 @@ SDLPoP-набор — 9. Предел 16 оставляет запас буду
|
||||
начала/длины на 128 байт;
|
||||
2. вычислить фактическое число страниц и отвергнуть набор больше 16;
|
||||
3. сформировать плотные 57 записей `page/off/len`;
|
||||
4. записать `SND/SOUND.IDX` в формате `PSI1`, включая фактическое число
|
||||
4. записать `SND/snd.idx` в формате `PSI1`, включая фактическое число
|
||||
страниц;
|
||||
5. собрать обычный `SND/SND.ARC` из полученных страниц;
|
||||
5. собрать обычный `SND/snd.arc` из полученных страниц;
|
||||
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
||||
|
||||
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
||||
@@ -517,13 +525,13 @@ Makefile, упаковщик звука предпочтительно долж
|
||||
|
||||
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
||||
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
||||
согласованную пару `SOUND.IDX` + `SND.ARC`.
|
||||
согласованную пару `snd.idx` + `snd.arc`.
|
||||
|
||||
## 20. Этапы реализации эффектов
|
||||
|
||||
### SI0 — формат и host-тест
|
||||
|
||||
- writer/reader отдельного `SOUND.IDX` в формате `PSI1`;
|
||||
- writer/reader отдельного `snd.idx` в формате `PSI1`;
|
||||
- проверка точного пятибайтового LE-формата записи;
|
||||
- сборка и разбор обоих имеющихся DIGISND-наборов;
|
||||
- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей.
|
||||
@@ -545,7 +553,7 @@ Makefile, упаковщик звука предпочтительно долж
|
||||
### SI3 — сборка без перелинковки
|
||||
|
||||
- убрать фиксированный `SND_ATL` из десяти имён;
|
||||
- сделать `SOUND.IDX` и `SND.ARC` согласованными результатами выбора набора;
|
||||
- сделать `snd.idx` и `snd.arc` согласованными результатами выбора набора;
|
||||
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
||||
|
||||
### SI4 — приёмка
|
||||
@@ -561,7 +569,7 @@ Makefile, упаковщик звука предпочтительно долж
|
||||
## 21. Критерии готовности эффектов
|
||||
|
||||
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
||||
- замена согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC` не требует
|
||||
- замена согласованной пары `SND/snd.idx` + `SND/snd.arc` не требует
|
||||
компиляции C и не меняет EXE;
|
||||
- `pop_snd_tbl` больше не генерируется как C-код;
|
||||
- загружается фактическое число страниц в диапазоне 1..16;
|
||||
|
||||
@@ -163,10 +163,16 @@ B имеет смысл только если C окажется неподъё
|
||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
||||
есть, и она влияет на выбор источника.
|
||||
|
||||
### Оцифровка: берём MSDOS
|
||||
### Оцифровка: по умолчанию SDLPoP, MSDOS — опция
|
||||
|
||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||
**С 2026-08-31 умолчание — SDLPoP** (`SND_SRC=sdlpop`): он выкачивается
|
||||
автоматически (`make fetch`), а оригинального дистрибутива DOS у сборки
|
||||
может не быть вовсе — и она обязана работать без него. У кого дистрибутив
|
||||
есть, включает лучший набор явно: `make SND_SRC=msdos`.
|
||||
|
||||
Платим за это ровно тем, что разобрано ниже. Заголовок в поставках разный
|
||||
(`digi_new_type` против `digi_type`), но **28 звуков из 31 совпадают
|
||||
побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||
|
||||
| id | звук | MSDOS | SDLPoP |
|
||||
|---|---|---:|---:|
|
||||
@@ -174,9 +180,13 @@ B имеет смысл только если C окажется неподъё
|
||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
||||
|
||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
||||
где источник наоборот SDLPoP.
|
||||
`spiked` в наборе SDLPoP фактически пустой: это звук насаживания на пики
|
||||
(`spiked()`, seg005.c:245, кадр 177), и с умолчанием он не звучит — от пик
|
||||
остаётся только шипение выдвижения (49 `spikes`). Звон клинков (10) на
|
||||
секунду короче, а 11 просто другая запись.
|
||||
|
||||
Всё остальное — включая уровни `LEVELS/res20xx.bin` — берётся из SDLPoP
|
||||
всегда: `MSDOS/` нужен ровно для этих трёх звуков и ни для чего больше.
|
||||
|
||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
||||
|
||||
@@ -961,3 +971,226 @@ load_intro у оригинала и так начинается с ожидан
|
||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||
за размер кольца — то есть подкачка успевает.
|
||||
|
||||
## 9. ЗАДЕРЖКА СТАРТА ЗВУКА: ЗАМЕР (2026-08-31)
|
||||
|
||||
Замерено в MAME на живой сборке: точка отсчёта — подстановка заявки
|
||||
отладчиком при ОСТАНОВЛЕННОЙ машине, конец — условный watchpoint
|
||||
`wpset 19FF6,1,w,{wpdata != 0}` на старший байт `pop_mus_left`, то есть
|
||||
момент, когда трек реально зазвучал. Масштаб тактов откалиброван не по
|
||||
памяти, а по аппаратным часам: период тика насоса CBL — ровно
|
||||
`128/10937,5 = 11,7029` мс, измерено 245 748 и 245 778 тактов, отсюда
|
||||
**21,00 МГц** (кадр луча — 429 973 такта, развёртка 48,8 Гц, не 50).
|
||||
|
||||
### 9.1 Эффекты стартуют без задержки, музыка — через ~200 мс
|
||||
|
||||
Разница принципиальная и вытекает из устройства:
|
||||
|
||||
* **Оцифрованные эффекты диска не касаются вовсе.** `pop_sfx_init()`
|
||||
вызывается ОДИН раз за запуск (`sprpop_cold.c`), вычитывает весь
|
||||
`SND/snd.arc` в EMM и закрывает файл. Дальше `pop_sfx_play` только
|
||||
номинирует, а `pop_sfx_tick` берёт `page/off/len` из таблицы в памяти.
|
||||
Задержка = остаток текущего кадра + ближайший запрос CBL (≤ 11,7 мс).
|
||||
Держать архив открытым НЕЧЕГО: после старта он не нужен.
|
||||
* **Музыка читается с диска на каждый трек** — `mNN.bin` лежат
|
||||
отдельными файлами, и `pop_music_load_begin` открывает свой.
|
||||
|
||||
### 9.2 Разложение окна музыки
|
||||
|
||||
От заявки (`pop_mus_req`) до звучания (`pop_mus_left != 0`), одна
|
||||
загрузка по шагам:
|
||||
|
||||
| шаг | цена |
|
||||
|---|---:|
|
||||
| ожидание `pop_music_service` в кадре | 22,6 мс |
|
||||
| `chdir` #1 (вход в `POP_PATH_CALL`) | **46,1 мс** |
|
||||
| `open` файла трека | 34,5 мс |
|
||||
| `chdir` #2 (восстановление каталога) | **59,0 мс** |
|
||||
| чтение первых 8 КБ (`bank_read_page`) | 34,0 мс |
|
||||
| **итого** | **196,7 мс** |
|
||||
|
||||
**Главное здесь: два `chdir` стоят 105 мс — больше половины окна, и
|
||||
каждый дороже самого `open`.** Убрать их нельзя: старые DSS оставляют
|
||||
рабочий каталог внутри последнего подкаталога, и без восстановления
|
||||
ломаются относительные пути (ради этого `POP_PATH_CALL` и заведён).
|
||||
Теоретический запас — снять ОДИН из двух (≈ 50 мс), но это риск для
|
||||
других мест, которые полагаются на восстановленный каталог.
|
||||
|
||||
Для музыки задержка непринципиальна: трек и так начинается на границе
|
||||
события. Критична она была бы для эффектов — там её нет по построению.
|
||||
|
||||
### 9.3 Разброс и режимы скорости
|
||||
|
||||
Семь прогонов: **189,3 … 232,8 мс**, среднее 200,1. Величина
|
||||
ФИЗИЧЕСКАЯ и от режима скорости не зависит, меняется только её пересчёт
|
||||
в логические кадры:
|
||||
|
||||
| режим | логический кадр (замерено) | окно в кадрах |
|
||||
|---|---:|---:|
|
||||
| NORMAL вне боя | 81,93 мс = 4,00 кадра луча | 2,31 … 2,84 |
|
||||
| FAST / FASTEST вне боя | 61,4 мс = 3,00 кадра луча | 3,09 … 3,42 |
|
||||
|
||||
Худший случай (232,8 мс в коротком кадре) — 3,79 логического кадра.
|
||||
Пейсер при этом работает ровно как задумано: `pop_pace_n` заказывает 4
|
||||
кадра луча вне боя при NORMAL и 3 при FAST/FASTEST, замер это
|
||||
подтвердил. Подкачка трека кадр НЕ просаживает.
|
||||
|
||||
### 9.4 Следствие для паузы конца уровня
|
||||
|
||||
Ждать окончания мелодии по одному `pop_music_busy()` НЕЛЬЗЯ: в этом окне
|
||||
он ещё ложен, и уровень сменился бы до первой ноты. Правильный признак
|
||||
— состояние «аудио-событие в работе», которое выводится из уже
|
||||
существующих переменных без новых сущностей:
|
||||
|
||||
```c
|
||||
uint8_t pop_music_active(void) __banked /* банк 9 — там ld_busy */
|
||||
{
|
||||
return (uint8_t)(pop_mus_req || ld_busy || pop_mus_left);
|
||||
}
|
||||
```
|
||||
|
||||
Переход «запрошен → грузится → звучит» снаружи непрерывен: `service`
|
||||
обнуляет `pop_mus_req` и тут же в `load_begin` взводит `ld_busy`, а
|
||||
главный цикл между этими операторами не выполняется.
|
||||
|
||||
Это лучше фиксированной задержки «на раскачку»: не нужна подобранная
|
||||
константа, нет зависимости от режима скорости, а при провале загрузки
|
||||
(нет файла, нет памяти) все три признака ложны и уровень меняется сразу,
|
||||
вместо слепого ожидания. Заодно чинится расхождение с SDLPoP при
|
||||
ВЫКЛЮЧЕННОМ звуке: `pop_sfx_play` возвращается рано при `!pop_snd_ok`,
|
||||
заявка не кладётся, паузы нет — как в оригинале (`seg006:651` ставит
|
||||
трек только `if (is_sound_on)`, а `seg003:387` ждёт
|
||||
`check_sound_playing()`). Сейчас `pop_endmus_left` взводится
|
||||
безусловно, и при выключенном звуке игра держит пройденный уровень
|
||||
лишние 12 с молча.
|
||||
|
||||
## 10. ЭФФЕКТЫ БЕЗ ПЕРЕЛИНКОВКИ: РАСКЛАДКА УЕХАЛА НА ДИСК (2026-08-31)
|
||||
|
||||
Сделана часть II плана `music_runtime_index_plan.md` (этапы SI0..SI4).
|
||||
Раскладка звукового набора больше не компилируется в EXE: пара
|
||||
`SND/snd.arc` + `SND/snd.idx` меняется целиком, бинарник остаётся прежним.
|
||||
|
||||
### 10.1 Что именно изменилось
|
||||
|
||||
`gen/pop_sound_tbl.h` УДАЛЁН — упаковщик его не печатает. Вместо него:
|
||||
|
||||
* `src/pop_snd_tbl.h` — рукописный: тип записи и инварианты программы
|
||||
(`POP_SND_COUNT` 57, `POP_SND_BLOCK` 128, `POP_SND_MAX_PAGES` 16,
|
||||
константы блока тишины). От набора не зависит НИЧЕГО из этого;
|
||||
* `src/pop_snd_data.c` — резидентные данные: `pop_snd_tbl[57]`,
|
||||
`pop_snd_page[16]`, `pop_snd_pages`;
|
||||
* `SND/snd.idx` — дисковый индекс, формат `PSI1` (писатель и разборщик —
|
||||
`tools/pop_idx.py`, покрыт `make test-tools`, 22 теста);
|
||||
* `pop_sfx_cold.c` — чтение и полная валидация индекса.
|
||||
|
||||
`POP_SND_PAGES` как compile-time размер набора БОЛЬШЕ НЕ СУЩЕСТВУЕТ. Его
|
||||
заменили две разные величины: `POP_SND_MAX_PAGES` — вместимость
|
||||
загрузчика (16, запас под чужие наборы), и runtime `pop_snd_pages` —
|
||||
сколько страниц у ЭТОГО набора. Насос сверяет переход длинного эффекта
|
||||
через границу страницы со вторым, а не с первым.
|
||||
|
||||
### 10.2 Формат `PSI1` и почему запись ровно 5 байт
|
||||
|
||||
Один сектор DSS: 16 байт заголовка (магия, число записей, размер записи,
|
||||
log2 блока, формат PCM, число страниц) и 57 записей по 5 байт —
|
||||
`page` (u8), `off` (u16 LE), `len` (u16 LE), — то есть побайтовый образ
|
||||
`pop_snd_tbl[]`. Записи читаются ОДНИМ `read` прямо в резидентную
|
||||
таблицу, без разбора по полям.
|
||||
|
||||
Поэтому `sizeof(pop_snd_ent_t) == 5` в ABI SDCC z80 — часть ДИСКОВОГО
|
||||
контракта, а не деталь компилятора. Ловится с двух сторон: статически
|
||||
(`pop_snd_data.c`) и полем размера записи в заголовке, которое загрузчик
|
||||
сверяет со своим `sizeof`.
|
||||
|
||||
### 10.3 Цена в памяти
|
||||
|
||||
Таблица была `static const` в `_CODE`, стала изменяемой в `_DATA`.
|
||||
Замер до/после:
|
||||
|
||||
| | было | стало | Δ |
|
||||
|---|---:|---:|---:|
|
||||
| `_CODE` | 23865 | 23582 | −283 |
|
||||
| данные | 7128 | 7421 | +293 |
|
||||
| куча резидента | 239 Б | 229 Б | −10 |
|
||||
| банк 8 | 14824 | 15425 | +601 |
|
||||
|
||||
Суммарный резидентный расход почти не изменился — в huge-модели `_CODE`
|
||||
и данные лежат подряд в одном диапазоне `0x4000..0xBFFF`. Реальные
|
||||
10 байт — это расширение `pop_snd_page` с размера набора до
|
||||
`POP_SND_MAX_PAGES` плюс байт счётчика.
|
||||
|
||||
### 10.4 Валидация: чему именно нельзя доверять
|
||||
|
||||
Индекс — файл на диске, и битый индекс увёл бы насос за пределы
|
||||
выделенного блока EMM. Проверяются заголовок целиком и КАЖДАЯ запись:
|
||||
`page < pop_snd_pages`, `off` внутри страницы, кратность `off`/`len`
|
||||
блоку 128, непересечение с блоком тишины и то, что длинный эффект не
|
||||
выходит за последнюю страницу набора.
|
||||
|
||||
Последняя проверка считается В БЛОКАХ, а не в байтах: байтовый адрес
|
||||
конца (`page * 16384 + off + len`) не влезает в `uint16`, а 32-битная
|
||||
арифметика на Z80 дорога. В блоках весь набор — не больше
|
||||
`16 * 128 = 2048`, и всё считается шестнадцатью битами.
|
||||
|
||||
Число страниц из индекса дополнительно сверяется с числом элементов в
|
||||
`snd.arc`: расходящаяся пара означает, что файлы из разных наборов.
|
||||
|
||||
### 10.5 Нет индекса — эффектов нет, но МУЗЫКА ОСТАЁТСЯ
|
||||
|
||||
Первая версия просто возвращала ошибку, и это оказалось непроходимой
|
||||
игрой: тишину в паузах насос льёт из первого блока набора, поэтому без
|
||||
набора CBL не открывался вовсе — а вместе с ним вставала музыка, чьи
|
||||
блоки отсчитывает тот же насос. Заставка ждала конца трека, который не
|
||||
начинал играть, и `pop_music_busy()` не гас никогда.
|
||||
|
||||
Теперь при отказе индекса поднимается ПУСТОЙ набор: одна страница EMM с
|
||||
блоком тишины. Все `len` нулевые, поэтому `pop_sfx_play` уходит по ветке
|
||||
«нет оцифровки» — эффекты молчат, музыка играет, игра проходима.
|
||||
|
||||
Две тонкости этой заливки, обе выяснены на живой машине:
|
||||
|
||||
1. **Заливается только первый блок (128 байт), а не вся страница.**
|
||||
`gfx_w0_page_prepare` ставит в страницу IRQ/NMI-стабы, чтобы
|
||||
прерывание при замапленном W0 нашло валидный код. Заливка всей
|
||||
страницы затирает их, и первое же прерывание уводит процессор в
|
||||
мусор — чёрный экран на старте. Насосу больше и не нужно: он берёт
|
||||
ровно блок тишины, да и то через W3.
|
||||
2. **Запись под `IRQ_DISABLE`** — 128 байт накрывают стаб RST38, и до
|
||||
`gfx_w0_unmap` прерываний быть не должно.
|
||||
|
||||
### 10.6 Грабли сборки: секундная гранулярность mtime
|
||||
|
||||
Смена `SND_SRC` тихо давала НЕВЕРНЫЙ результат: `sdlpop -> msdos ->
|
||||
sdlpop` перепаковывал `build/atl`, а в `assets/packed` оставался прежний
|
||||
набор. Причина не в логике, а во времени: упаковка укладывается в одну
|
||||
секунду с записью stamp'а, а `make` считает цель, сделанную в ту же
|
||||
секунду, что и зависимость, актуальной. С паузой в секунду не
|
||||
воспроизводится вовсе.
|
||||
|
||||
Лечится двумя приёмами, и оба убирают время из решения:
|
||||
|
||||
* смена варианта СНОСИТ stamp'ы своего семейства (`rm -f
|
||||
$(STAMP)/sound-*`) — нет файла, значит правило обязано выполниться;
|
||||
* упаковка, сборка архива и копия индекса делаются ОДНИМ рецептом:
|
||||
внутри рецепта сравнивать нечего.
|
||||
|
||||
То же лечение получила и музыка (`MUSIC_FMT`) — механизм общий.
|
||||
|
||||
### 10.7 Что проверено на живой машине
|
||||
|
||||
* таблица в памяти совпадает с файлом из образа ПОБАЙТОВО (285 байт);
|
||||
* один и тот же EXE (sha256 не меняется) поднимает SDLPoP (9 страниц) и
|
||||
MSDOS (10 страниц);
|
||||
* СТРОГАЯ проверка: у двух реальных наборов совпадают 23 записи из 31,
|
||||
поэтому сделан отладочный ключ `pop_pack_sound.py --order reverse` —
|
||||
набор с обратным порядком укладки, где от штатного отличаются 30 из 31
|
||||
записей. Игра на нём звучит правильно, значит раскладка берётся с
|
||||
диска, а не из памяти;
|
||||
* битый индекс (испорченная магия) — эффекты выключаются, игра жива;
|
||||
* отсутствующий индекс — музыка играет, эффектов нет, PV-сцена проходит;
|
||||
* Ctrl+S в обоих режимах работает штатно.
|
||||
|
||||
Маркер для ручной проверки: звук 51 `princess_door_opening` (дверь в
|
||||
сцене с принцессой) лежит у наборов по РАЗНЫМ страницам — 8 у SDLPoP,
|
||||
9 у MSDOS. А вот 4 `gate_closing` из той же сцены совпадает побайтно и
|
||||
для проверки подмены не годится.
|
||||
|
||||
Reference in New Issue
Block a user