Files
Sprinter-SDCC/applications/SprPoP/docs/ideas_backlog.md
T
snark13 2349481b86 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
2026-08-31 16:14:53 +03:00

184 lines
16 KiB
Markdown
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)
Не план работ, а список того, что осознанно отложено: каждая запись —
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
сделать это прямо сейчас.
## Зелье «переворот экрана» (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).
Так что это оптимизация, а не исправление.