ТАЙМИНГИ. Пользователь заметил, что заклинание Джафара не совпадает с
музыкой. Замер в MAME (breakpoint на вспышке + курсор трека): реплика m53
успевала ДОИГРАТЬ до молнии, хотя по шкале оригинала между входом Джафара
и заклинанием ровно 822 тика = 13,7 с.
Причина: пейсинг считал НАШИ ожидания vsync, а не прошедшее время.
Полноэкранная копия страницы стоит больше кадра луча, и разница копилась.
Кадровые прерывания для счёта не годятся (теряются в di-окнах
акселератора), поэтому часами стал насос CBL: он идёт от расхода буфера
железом, 85,4 Гц, и ему безразлично, чем занят главный цикл. Один тик
оригинала = 57/40 тика насоса (0,07 % ошибки, без деления в кадре).
Когда звук выключен, работает прежний путь по кадрам луча.
После фикса замер даёт 229 блоков остатка m53 против расчётных 233 —
расхождение 47 мс.
АРХИВЫ. Группы ресурсов сложены в PBA1: kid (58 атласов), pv (78),
shadow (32), bg (25), title (20), guard (10), vizier (5), skel (4). Было
232 файла на образе, стало 8 архивов плюс шесть палитр и таблицы уровней.
При цене `open` 51,4 мс против 32,6 мс за чтение 16 КБ это возвращает
секунды на каждой загрузке.
- pop_pack_arc.py печатает индексы элементов константами (ARC_<GRP>_<FILE>),
порядок задаёт Makefile, а серии код проверяет статически;
- atlas_load разделён: atlas_attach принимает уже прочитанную страницу,
поэтому чтение из архива не дублирует проверку магии и подготовку W0;
- имена архивов живут в pop_arc.c и адресуются номером. Строковый литерал
лежит в rodata своего банка, и указатель на него из другого банка после
переключения W3 показывает на чужие данные — ровно так падала загрузка
фона (поймано брейкпоинтом на puts).
Проверено в MAME: заставка, интро и уровень 1 собираются из архивов,
Тень грузит все 32 страницы. Host-тесты: 15 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ЗАМЕР, из которого всё выросло (MAME, 2026-08-25): `open` стоит 51,4 мс,
а чтение 16 КБ из уже открытого файла — 32,6 мс. То есть 61 % времени
загрузки уходит на ОТКРЫТИЕ, а не на данные, и чистая скорость чтения —
491 КБ/с, а не 260. На диске игры 343 файла; одна только стартовая пачка
(kid+shadow+bg+sound+guard, ~142 файла) — это 7,3 секунды чистого open.
* toolchain/pop_pack_arc.py + pop_arc.c/h (банк 8) — формат PBA1:
заголовок 512 Б, магия, count, записи по 4 байта (offset в СЕКТОРАХ —
архив бывает больше 64 КБ; size в байтах — ресурс всегда <= 16 КБ,
он обязан лезть в EMM-страницу). Таблицу держит вызывающий на своём
стеке: в W2 её класть нельзя, а стека в точке загрузки израсходовано
75 байт из 1279 (замер там же).
* libc: bank_read_page — чтение из УЖЕ ОТКРЫТОГО файла в EMM-страницу.
bank_load_file переписана через неё, поэтому дублирования работы с W3
не осталось и резидент почти не вырос.
* Набор эффектов: 10 файлов -> SND\snd.arc, один open вместо десяти.
* Музыка: трек лежит ОДНИМ файлом и читается порциями по странице —
нарезка на куски больше не нужна. Этот же путь понадобится финалу
`won` (1,2 МБ), который в память целиком не влезает.
* Менеджер звука приведён к оригиналу: играющая музыка участвует в
таблицах приоритета наравне с эффектами (seg000:1672 + data.h:433).
Почти вся музыка НЕперебиваема, поэтому поверх неё эффект не стартует —
ровно как в SDLPoP; раньше эффект её перебивал.
* ALLOCS 3000 -> 6000 (решение пользователя): сборка +20 с, резидент
-161 байт. Куча 87 -> 248 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
roomtest:
- QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD,
транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная
сумма payload'а; сериализация всех игровых переменных через W0-примитивы
pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон
- pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки
(карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация
кэшей спрайтов и HP)
- сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai
(+ восстановление инвариантов: mobs_live, trob_drawn, redraw)
- immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а
libc/libbgi:
- bank_load_file()/bank_save_file() — резидентное файловое I/O в банк,
без правила W3 (путь читается до переключения страницы)
- gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной
функцией; atlas_load.c и roomtest переведены на новые примитивы;
ручные ISR-стабы удалены
sprinter-cc / сборка:
- --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные
const n_banks удалены из тестов
- roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один
запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS
Линкер тянет .rel целиком, поэтому malloc/free, стоявшие в мёртвой ветке
CBL_UNDERRUN_SILENCE внутри cbl_open, приезжали в резидент КАЖДОМУ
приложению — включая те, что льют тишину сами и кучей не пользуются.
Разведено:
cbl_open(freq, fmt, pump, fill) — ничего не аллоцирует;
cbl_open_silence(freq, fmt, pump, fill) — аллоцирует буфер тишины;
_cbl_open_raw(...) — общее тело.
Параметр underrun_mode из публичного API убран: режим задаёт выбор функции.
cbl_close больше не зовёт free — иначе malloc возвращался бы тем же путём.
Буфер тишины живёт до выхода из программы и переиспользуется; его размер
запоминается, иначе открытие 16-бит после 8-бит писало бы memset'ом мимо
выделенного куска. _cbl_open_raw указатель на буфер не трогает вовсе —
иначе cbl_open после cbl_open_silence терял бы уже выделенную память.
В дереве режим SILENCE не использовал никто: все три вызова (cblwav,
cblstream, PoP) передавали CBL_UNDERRUN_APP.
Итог для PoP: _CODE 24329 -> 23716 (−613 Б), свободно в резиденте 129 ->
747 Б. make size-check чистый (67 программ), звук в MAME проверен —
насос отработал 696 запросов, pop_snd_ok/want = 1/1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт Ctrl+S из SDLPoP (seg000:657). Выключение реально закрывает CBL, а
не глушит сэмпл: иначе насос продолжал бы отдавать блоки тишины и платить
те же 3,26 % процессорного времени.
Флаг намерения pop_snd_want отдельно от pop_snd_ok: последний гасит
служебная пауза на время загрузки уровня, и правь Ctrl+S только его —
первая же смена уровня вернула бы выключенный звук.
Индикация: пурпурная палочка в борте, когда звука НЕТ (включённый слышно
и так, а молчание неотличимо от «нечему звучать»).
Чит S «выдать меч» был отладочным и больше не нужен — снят, клавиша ушла
под звук.
Замер цены звука — sound_plan.md §9: 8 001 такт на прерывание при периоде
245 759 (3,26 % времени), +2,2 % к работе кадра в сцене 11/15, период
кадра не сдвинулся ни разу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота
железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 —
размер блока запроса CBL, поэтому ни один блок не пересекает границу
страницы и проигрыватель не знает слова «стык».
Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих
мест физики и из play_seq. Тишину льём свою (первый блок набора), а не
через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная.
Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от
pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и
железо крутит хвост буфера — на слух скрежет.
Разведены все места play_sound() SDLPoP, у которых есть оцифровка
(id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и
провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч,
зеркало, скелет, зелье. Таблица соответствий — docs/sound_plan.md §8.
Музыкальные id остаются с нулевой длиной до фазы музыки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>