Commit Graph

8 Commits

Author SHA1 Message Date
snark13 df5071a967 SprPoP: музыка без перелинковки — длины и длительности уехали на диск
Часть I плана music_runtime_index_plan.md (MI0..MI5).  gen/pop_music_tbl.h
и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик
читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в
make test-tools).  Один и тот же sprpop.exe работает с любым из четырёх
наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется.

ГДЕ ЖИВЁТ ИНДЕКС.  228 байт таблицы в W2 не положить (свободной кучи там
порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в
резиденте от него два байта.  Данные в странице — со смещения 0x100:
gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля
они попали бы прямо в записи id 10 и 21.  Со смещением работает штатная
защита, а не запрет прерываний (тот же приём, что CFG_BASE в
pop_config.c).  Число страниц в индексе не хранится — считается из blocks,
чтобы не разъехалось.

ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ.  pop_endmus_left и
POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() —
«заявка лежит, идёт загрузка или трек звучит».  Одного busy мало: между
заявкой и первой нотой 190-230 мс (замер в sound_plan §9).  Прежний
счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого
делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3
кадра луча, а не 4).  Побочно исправилось расхождение с SDLPoP: при
выключенном звуке заявка не кладётся, и уровень меняется сразу, как в
оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный
уровень лишние 12 секунд в тишине.

PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из
индекса при входе в сцену; прежние выражения шкалы не изменились.  План
предлагал протащить структуру времён через пять функций — для сцены,
которая идёт раз за запуск, это того не стоит.

ПАМЯТЬ.  За обе фазы резидент не вырос, а освободился: _CODE 23865 ->
23544, куча 239 -> 256 Б.  Банк 9 похудел на 118 Б (ушла pop_mus_tbl из
rodata), банк 11 — на длительности реплик.

ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в
индексах различаются, и контрольные значения совпали с предсказанными
планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462
блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его
длительностям; без mus.idx музыки нет, эффекты работают, игра проходима.

НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в
игре.  Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 17:17:59 +03:00
snark13 602c3a20fa SprPoP: сняты последние глушения насоса — палитра на смене уровня и загрузки треков
Замер тем же способом (брейк на pop_sfx_fill + печать totalcycles, три окна
по 800 вызовов = 9 секунд каждое, с уже снятыми глушениями):

  период насоса   245 760 тактов (медиана во всех окнах)
  максимум        245 832 / 270 096 / 311 346 (1,00 / 1,10 / 1,27 периода)
  пропущено порций 0

Порция считается пропущенной, когда зазор доходит до ДВУХ периодов: сама
порция отдаётся железу за период до того, как она понадобится, поэтому
опоздание обработчика на 1,1 мс — джиттер, а не потеря.  Запас
десятикратный.

Поэтому сняты и оставшиеся места:
* палитра через BIOS на переходе БЕЗ катсцены (sprpop_cold.c) — то самое,
  где ловили скрежет 2026-08-25; в комментарии помечено, что при возврате
  скрежета возвращать надо именно сюда;
* массовые чтения треков и ресурсов под чёрным экраном (pop_intro.c):
  первая реплика PV, трек заставки между уровнями, «время вышло», ресурсы
  финала.

Осталось только то, что глушит звук ПО СМЫСЛУ, а не ради защиты: выходы из
сцен (pop_music_free + pause), уход в титры и в игру, выключение по Ctrl+S.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 19:01:27 +03:00
snark13 3bf28da8ee SprPoP: трек заставки переживает загрузку уровня — порядок как в оригинале
ЗАМЕР (MAME, 2026-08-28).  Брейк на pop_sfx_fill с печатью totalcycles, 500
подряд вызовов насоса через всю загрузку уровня 1->2 под звучащий трек 27:
медиана интервала 245 760 тактов, максимум 245 832 при дедлайне 251 000
(11,7 мс) — НИ ОДНОЙ пропущенной порции.  Загрузка уровня насос не морит.

Поэтому снята двойная заплатка:
* pop_level_switch больше не глушит насос перед pop_level_load_num;
* pre_cut_finish больше не досиживает трек на чёрном экране (это делалось
  только чтобы глушение не обрубило его на полуслове; ценой были ~8 секунд
  пустого экрана — трек 27 длиннее сцены: 10,7 с против 2,6 с).

Взамен восстановлен порядок оригинала (seg003:68-108, play_level):
катсцена возвращает управление сразу -> уровень грузится ПОД музыку ->
ожидание конца трека на чёрном экране (порт `while (check_sound_playing())`
+ stop_sounds) -> показ уровня.  Общая чернота теперь max(трек, загрузка), а
не их сумма, и трек не обрывается.  Ожидание со страховкой на ~20 с, чтобы
потоковый трек не подвесил переход.

Пропуск сцены по-прежнему обрывает музыку — как и было.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:53:44 +03:00
snark13 50570881f5 SprPoP: подгонка молнии под музыку снята — причина устранена
PV_MAGIC_LEAD двигал жест заклинания (замах, шаг назад, вспышка) на 100
тиков (1,67 с) раньше сценария: сцена была render-bound, шла ~49 тиков/с
вместо 60, а реплика играла по реальному времени — кода приходила раньше
молнии.  После перевода сцены на единые часы и блочную отрисовку подгонка
стала вредной: молния била больше чем на секунду РАНЬШЕ коды (проверка
пользователем).  Ставим 0; константу оставляем на месте — если запись
другого набора (mt32/ogg) разъедется, крутить надо её.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:28:17 +03:00
snark13 f25ed37d85 SprPoP: единые часы в сцене с Джафаром + конец уровня ждёт свою мелодию
1. СЦЕНА С ДЖАФАРОМ — ОДНИ ЧАСЫ, КАДРЫ ЛУЧА.  Было две шкалы, и выбор между
   ними делался гонкой на старте сцены: одна выборка pop_snd_tick через кадр,
   «успел ли диск раскрутить звук».  От прогона к прогону сцена шла то по
   тикам насоса CBL, то по кадрам луча, и кода реплики приходилась каждый раз
   на другое место картинки (наблюдение пользователя).  Насос был нужен
   потому, что кадр рисовался дольше своего интервала; теперь отрисовка
   разложена по интервалам (pv_restore_bg), и счёт кадров честен — ветка
   насоса убрана целиком.

2. ПОДКАЧКА ТРЕКА — ПОЛСТРАНИЦЫ ЗА ШАГ (pop_music_load_step).  8 КБ ≈ 16 мс
   влезают в кадровый интервал, целая страница (33 мс) не влезала и
   растягивала кадр сцены.  В сцене шаг остаётся безусловным (иначе реплики
   не успевали грузиться, memory pv_music_stall_regression) и оплачивается
   ровно одним интервалом.

3. КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ.  Оригинал (seg003:387, play_level_2) не
   сменяет уровень, пока `check_sound_playing()`: экран пройденного уровня
   живёт с анимацией факелов, пока звучит трек.  Мы уходили на смену сразу и
   обрывали мелодию на первых нотах.  Теперь ждём большего из двух:
   pop_music_busy() и счётчика pop_endmus_left по длине записи
   (gen/pop_music_ticks.h) — второе нужно потому, что при ВЫКЛЮЧЕННОЙ музыке
   busy ложен, а оригинал выдерживает паузу и молча.

Стартовый уровень возвращён на 1 (отладочный LEVEL=14 был только для замера).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:19:43 +03:00
snark13 3449f6f8c9 SprPoP: кадр катсцены — блоками по кадровому интервалу, а не «отрисовка плюс пять»
Катсцены шли ~8,2 fps вместо десяти.  Делитель тут ни при чём: у оригинала
cutscene_frame_time = 6 тиков по 1/60 с (reset_cutscene, seg001:527; его
зовёт load_intro прямо перед сценой) = 100 мс, у нас 5 кадров луча по 50 Гц
= те же 100 мс.  Причина в том, ГДЕ отсчитывался интервал: сначала рисовали
кадр целиком, и только потом ждали vsync и ещё четыре — то есть отрисовка
ПРИБАВЛЯЛАСЬ к делителю.  Полноэкранная gfx_copy_page стоит ~547 000 тактов
= 1,27 кадра, отсюда 6+ кадров вместо 5 (замер PV-RENDER-BOUND: 49 тиков/с
вместо 60).

Теперь отрисовка разложена на блоки, каждый из которых заведомо влезает в
кадровый интервал, и после каждого честно ждём vsync:
  фон верхняя половина -> vsync | фон нижняя половина -> vsync |
  актёры и декорации -> vsync (+ флип) | служебный блок (звук, подкачка
  трека) и добор до CUT_FRAME_VSYNC.
Фон восстанавливаем только по картинке (200 строк с POP_YOFF), а не по всем
256: сверху и снизу чёрная рамка.  Общий хелпер pv_restore_bg на все три
цикла — cut_run (сцены 8/9/12 и финал), pre_room_animated (2_6/4/12 и
time_expired) и intro_pv_draw_frame (сцена с Джафаром); последний теперь
возвращает 3 кадра вместо 1 (13 вместо 11 со вспышкой), вызывающий их и так
учитывал.

Сверка делителей с SDLPoP: у всех сцен 6 тиков = 5 наших кадров; плавает
только pv_scene (6 -> 8 -> 7, seg001:434/455) — это уже сделано кумулятивно
через pv_seq_period + POP_T60, и минимальный бюджет (5 кадров) больше трёх
съедаемых блоками.

Замер в MAME пока НЕ сделан: до финальной сцены на отладочном старте
LEVEL=14 не добраться (решётка перед комнатой 5 закрывается по таймеру, а
Ctrl-комбинации через MCP-мост до игры не доходят).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:55:27 +03:00
snark13 808c2a5349 SprPoP: заставку можно прервать в любой её точке, F10 — выход
Три места, где нажатие раньше не работало или работало наполовину.

FADE.  fade_run() был замкнутым циклом на 2,13 с без опроса клавиш, а на
стыке экранов их два: пропуск работал внутри сценария, но не между его
шагами, и заставка ощущалась невыключаемой.  Добавлены прерываемые
варианты (pop_ui_fade_*_skip, обёртки в pop_pal) — отдельными функциями,
а не флагом в прежних: в меню паузы и на переходе уровня прерывать
нечего, и менять там поведение молча не следует.  Прерванный fade всё
равно доводит палитру до конца, экран не остаётся на промежуточной
ступени.  Подключено в интерпретаторе сценария, сцене с принцессой,
четырёх катсценах cut_*, титрах и таблице рекордов.

ПРОЯВЛЕНИЕ ПОЛОСАМИ.  pop_screen_present_ltr() была void и нажатие
ГЛОТАЛА: полосы схлопывались, картинка появлялась целиком — и всё,
вызывающий о нажатии не узнавал.  На заставке это выглядело как
«клавиша срабатывает наполовину».  Теперь возвращает признак прерывания,
и он проброшен по маршруту: первый экран истории, титры финала, логотип
между «свадьбой» и титрами.

Везде считается КРОМКА нажатия от входа, как в сценах: клавиша, которой
закончили предыдущий экран, ещё зажата, и принимать её за новое нажатие
нельзя — иначе весь маршрут заставки схлопывался бы сам собой.

F10 — НЕМЕДЛЕННЫЙ ВЫХОД, откуда угодно.  Проверка стоит в kbd_idle(), а
этот хук висит на gfx_wait_vsync, то есть вызывается везде, где программа
ждёт кадр: заставка, титры, fade, проявление полосами, меню, игра.  Одна
точка вместо десятка по циклам ожидания.  Флаг pop_quit_req резидентный —
взводится и читается без трамплина из любого банка.  Проверка в НАЧАЛЕ
витка автомата обязательна: заставку прерывает любая клавиша, и без неё
F10 успевал уронить программу в загрузку уровня перед закрытием.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:49:01 +03:00
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00