2349481b86
Часть 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
184 lines
16 KiB
Markdown
184 lines
16 KiB
Markdown
# Идеи и вопросы «на подумать» (PoP)
|
||
|
||
Не план работ, а список того, что осознанно отложено: каждая запись —
|
||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||
сделать это прямо сейчас.
|
||
|
||
## Зелье «переворот экрана» (upside-down)
|
||
|
||
**Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а
|
||
кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради
|
||
бесплатного горизонтального зеркала). Значит вертикальный переворот для
|
||
персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить,
|
||
надо знать три факта.
|
||
|
||
**Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4
|
||
(`proc_get_object`, `seg006.c:1885` → `toggle_upside()`). Скан всех уровней
|
||
по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4
|
||
встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3
|
||
(перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не
|
||
нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс,
|
||
а точное соответствие данным уровней 1..8.
|
||
|
||
**Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты.
|
||
`flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет
|
||
местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей
|
||
внутри строки не трогает — это вертикальное зеркало, не поворот на 180°).
|
||
Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул
|
||
буфер → дорисовал → перевернул обратно. Так что в оригинале это
|
||
post-process всего экрана, и вопрос «как перевернуть колоночный спрайт»
|
||
там просто не возникает.
|
||
|
||
**Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага
|
||
«готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон
|
||
из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это
|
||
320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок.
|
||
|
||
**Варианты, которые надо будет взвесить (не сейчас):**
|
||
1. **Предпечённые перевёрнутые атласы.** Второй набор кадров
|
||
Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже
|
||
есть транспонирование — добавляется одной строкой). Рантайм: выбор
|
||
набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при
|
||
бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь.
|
||
2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся
|
||
непрерывным accel-прогоном, меняется только адрес назначения; цена —
|
||
вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать.
|
||
3. **Аппаратная помощь** — до проектирования проверить, есть ли у
|
||
акселератора направление копирования «вниз» (обратный инкремент адреса);
|
||
если есть, вариант 1 может и не понадобиться. Смотреть
|
||
`docs/new/06-accel.md` и `docs/reference/accel_r.txt`.
|
||
|
||
**Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже
|
||
будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него
|
||
выбирать между вариантами выше бессмысленно.
|
||
|
||
## Заменить генератор псевдослучайных чисел
|
||
|
||
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
||
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
||
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
||
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
||
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
||
|
||
## Отключать мышь на время игры
|
||
|
||
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
||
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
||
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
||
который у нас и без того занят на 86 %.
|
||
|
||
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
||
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
||
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
||
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
||
измеримый, просто в тесте он был наведён извне.
|
||
|
||
**Что проверить.**
|
||
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
||
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
||
«hide cursor» и снимает ли она обработчик.
|
||
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
||
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
||
3. Не ломает ли отключение выход в DSS: состояние обязано
|
||
восстанавливаться при `exit`, включая аварийный (atexit).
|
||
|
||
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
||
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
||
после выхода — заметный. Делать после того, как закроем стражей и
|
||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||
возможный возврат `pop_bg` в резидент `--w3`).
|
||
|
||
## Готовить следующий уровень, пока играет мелодия конца текущего
|
||
|
||
**САМАЯ ДАЛЬНЯЯ ВЕРСИЯ.** Не полишинг и не порт: это улучшение ПРОТИВ
|
||
оригинала. Планируем, но не раньше, чем закроем уровни и полишинг.
|
||
|
||
**Идея (пользователь, 2026-08-28).** Пока звучит мелодия конца уровня (и
|
||
трек заставки между уровнями), экран не меняется — значит в это время можно
|
||
успеть прочитать с диска следующий уровень, чтобы после музыки он появлялся
|
||
сразу, а не через секунду-другую загрузки.
|
||
|
||
**Что УЖЕ сделано и мерено (2026-08-28).** Половина этого уже работает:
|
||
трек заставки играет ПОВЕРХ загрузки уровня — так же, как в оригинале
|
||
(`seg003:68-108`: `load_intro` возвращает управление, `load_level()` идёт под
|
||
музыку, и только потом `while (check_sound_playing())` на чёрном экране).
|
||
Насос это переживает: 500 подряд вызовов `pop_sfx_fill` через всю загрузку —
|
||
максимальный зазор 245 832 такта при дедлайне 251 000, ни одной пропущенной
|
||
порции. Осталась вторая половина: мелодия конца уровня досиживается на
|
||
ЖИВОМ экране пройденного уровня (как в оригинале, `play_level_2`), и только
|
||
после неё начинается загрузка.
|
||
|
||
**Оговорки, найденные при разборе.**
|
||
|
||
1. **Загрузку придётся разрезать надвое.** Сейчас `pop_level_switch` мешает
|
||
дисковую работу (файл уровня в EMM-страницу, атласы стража и тайлсета) с
|
||
экранно-палитровой (`pop_bg_load` переписывает записи палитры и атласы,
|
||
`pop_guard_load` физически применяет цветовые слоты, обе страницы
|
||
заливаются, `pop_pal_black`). Вторую половину НЕЛЬЗЯ выполнять, пока на
|
||
экране ещё живёт пройденный уровень — иначе палитра поедет прямо на
|
||
картинке. То есть в «музыкальное окно» можно вынести только диск, а всё,
|
||
что трогает палитру и VRAM, остаётся после.
|
||
2. **Бюджет кадра.** Ожидание мелодии идёт в игровом цикле (гейт
|
||
`pop_endmus_left` + `pop_music_busy()`), кадр там обычный, 80-100 мс.
|
||
Шаг подкачки должен быть такого же размера, как музыкальный — полстраницы
|
||
(~16 мс), а не страница целиком; иначе кадры на экране пройденного уровня
|
||
начнут дёргаться.
|
||
3. **Память.** Страницы следующего уровня придётся держать одновременно со
|
||
страницами текущего — проверить EMM-бюджет (memory `sprinter_emm_budget`),
|
||
тяжёлые тут не файл уровня, а атласы тайлсета и стража.
|
||
4. **Риск невелик, но он в самом хрупком месте.** Палитро-атласный обмен —
|
||
источник уже пойманных багов (`PAL-DUNGEON-STALE`, `PAL-L1-AFTER-INTRO`),
|
||
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||
|
||
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||
|
||
## Музыка одним постоянно открытым архивом (замер 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).
|
||
Так что это оптимизация, а не исправление.
|