Commit Graph

437 Commits

Author SHA1 Message Date
snark13 5756101d29 Статус-строка: LEVEL N и лейбл QS/QL до дисковой операции
* pop_status_level() — порт show_level (seg008:25A8): «LEVEL N» на старте
  уровня.  Демо-уровень 0 и номера от 14 молчат, тринадцатый показывается
  двенадцатым, бесшовный переход 12->13 пропускается и гасит флаг за собой.
  Первое сообщение с ПАРАМЕТРОМ: строка собирается вручную (dec2, без
  printf и без деления), номер хранится снимком st_arg — иначе вторая
  страница дабл-буфера нарисовала бы другое число.
* pop_status_show_now() — печать немедленно, в ВИДИМУЮ страницу.
  QuickSave/QuickLoad заявляют лейбл ПЕРВЫМ действием, до mem_alloc/ESTEX:
  запись снимка занимает доли секунды, и раньше игрок видел сначала
  необъяснённый фриз, а надпись — уже после него.  Отказ переписывает
  строку на NO QUICKSAVE/NO QUICKLOAD обычной заявкой.
  Расхождение с оригиналом (он печатает по результату) — impl_diff.md.

Проверено в MAME: watchpoint ловит заявку внутри pop_status_show_now,
step_out возвращает в pop_qsave_process — и QUICKSAVE уже на экране, диск
ещё не тронут.  Бюджет прежний: _CODE 23981, куча 266 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:13:48 +03:00
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00
snark13 7fd7f28ffc Доки: план меню (рендер, restart без подтверждения), QSAVE закрыт
menu_settings_plan.md:
- §10 переписан: выбран Вариант A — текстовые строки + собственный
  растровый рендерер в новом банке; референс SDLPoP (hc_small_font /
  hc_font — один рендерер, два шрифта); шрифт как ассет из паковщика,
  прототип MS4 — системный CP866 ZG; двуязычность eng/rus через пару
  (таблица строк CP866, файл шрифта)
- Restart Level / Restart Game выполняются сразу, без подтверждения
  (§3, §8, из §11 убраны диалоги RESTART *?)
- §13 MS4: текстовый рендерер + два шрифта; §14: host-тест рендерера

quicksave_plan.md: статус «РЕАЛИЗОВАНО и проверено в MAME»
(v0.6-pop-quicksave), документ оставлен справочником по формату 'POPQ'

TASKS_OPEN/TASKS_CLOSED: запись QSAVE переехала в закрытые с полным
протоколом; docs/README.md аннотации обновлены
2026-08-22 12:45:01 +03:00
snark13 f4b4852d51 QuickSave F6/F9 в roomtest; bank_load_file/bank_save_file/gfx_w0_page_prepare; sprinter-cc: авто n_banks
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
v0.6-pop-quicksave
2026-08-22 11:53:10 +03:00
snark13 b6699b3aef Разрез pop_tile: холодная половина в банк 5 (−1788 Б резидента)
pop_tile.c был крупнейшим жильцом резидента (5972 Б кода).  Целиком он не
уедет — его const-таблицы читают банки 2, 3, 7 и 8, а таблица в чужом банке
не видна.  Поэтому разрез, а не перенос.

Отбор ЗАМЕРОМ, а не по смыслу: каждая функция посчитана брейкпоинтом-
счётчиком в MAME — сколько вызовов в кадре покоя и сколько в кадре полной
перерисовки комнаты (форсируется читом +/-).  Порог — пик не больше 3.
Проверено на ДВУХ тайлсетах, подземелье и дворец: pop_mem_b рисует
композитный кусок и мог оказаться дворцовым, но и там 0 вызовов.

Уехало: pop_mem_b, pop_cd_hit (+hit_rect), pop_cd_hit_slot, pop_cd_init,
pop_cd_clear, pop_t_win_set/clear, pop_bar_black, pop_heal_off,
pop_potion_flask, pop_room_set_above/below.
Осталось: pop_blit_b со статиками (184 вызова на редрав), pop_cd_touch
(198), pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
pop_tile_mod (70), все таблицы.  pop_fore_set_clip оставлен намеренно —
88 Б не стоят отказа от прямого вызова из банка 4.

Цена трамплина замерена: 252 такта пролог + 84 эпилог + ~50 у вызывающего
= ~410.  Это ~1000 тактов на кадр покоя (0,2 % работы) и ~3700 на редрав.

blit_b_clip перестал быть static и объявлен в _pop_tile.h: вызов
банк -> резидент прямой, трамплин не нужен, поэтому статик горячей половины
переносить следом не пришлось.

Итог: _CODE 23716 -> 21928, свободно 747 -> 2535 Б (с 129 Б до всех работ).
Проверено в MAME на уровнях 1 и 4, с переходами комнат.

Метод замера, таблица частот и ловушка с данными банка — docs/resident_budget.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:26:05 +03:00
snark13 536c60d14c AGENTS.md + .codex/config.toml — конфигурация агента
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 e2730f78e0 gfx_scroll_v: колоночный accel-проход без буфера-посредника + тест scroll
Вертикальный скролл перестал ходить через строку-буфер на стеке: колонку
читаем с Port_Y=ys, пишем с Port_Y=yd, а STOP между read- и write-триггером
делает промежуточный OUT Port_Y безопасным.  Один проход вместо
grab→blit-через-буфер.

Справочник libc приведён в соответствие (там же строка про новую точку
входа cbl_open_silence).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 3ea4546656 cbltest: пропущенный вызывающий cbl_open + обновление размерного эталона
tests/cbltest не попал в правку API (мой греп обрезался на артефактах
сборки, полный make его и поймал).

Эталон размеров: cblstream −614, cbltest −617, cblwav −592 — это ушедший
malloc.  bgi_img +229 к моей правке отношения не имеет (CBL он не линкует
вовсе): рост пришёл с b3e754a, реверта «каталог атласа читается из W0».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 8458ec65f4 libc/cbl: две точки входа вместо underrun_mode — malloc больше не в резиденте
Линкер тянет .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>
2026-08-20 21:53:08 +03:00
snark13 63e426cc0d Откат оптимизации резидента: перенос данных банка в его страницу ломает картинку
Возврат к состоянию после звуковых правок (c6828c0).  Отменяются 3545826 и
9025573 целиком: --bank-data=SRC в sprinter-cc, его включение в PoP и
docs/resident_budget.md.

Что выяснено и почему откат, а не доводка.  Перенос писучих данных
банкового модуля в его 16-КБ страницу даёт цветной мусор блоками и уводит
DSS.  У pop_trob причина найдена: pop_trob_modif() возвращает указатель на
room_modif[24][30], и его разыменовывают банки 2/3/7 и резидент — то есть
пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода.  Def/Ref-анализ такое
не ловит: снаружи ссылки на символ нет, есть ссылка на функцию, отдающую
его адрес.

Но и один pop_room, у которого явной утечки указателя найти не удалось,
ломается так же — значит механизм понят не до конца.  Пока не понят,
включать нельзя.  Нулевая инициализация при этом ни при чём: mkexe -p 0
проверен по образу (прогон нулей 14304 Б, самый длинный прогон 0xFF — 14).

Место в резиденте искать другими путями: malloc (287 Б, требует раздельных
cbl_open для APP и SILENCE) и код pop_tile (5972 Б).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.0.3-prealpha-soundeffects
2026-08-20 21:26:23 +03:00
snark13 90255737c2 Откат bank-data для pop_trob: указатель на его статику уходил наружу
Симптом: через несколько комнат живого прохода перезагружался DSS.

pop_trob_modif() возвращает указатель на room_modif[24][30], а зовут её из
банков 2, 3, 7 и резидента.  После переноса массив лежит по 0xC000+ в
странице банка 6, но разыменовывает указатель ЧУЖОЙ код — когда замаплена
его собственная страница.  Значит чтение и запись идут поверх кода соседнего
банка.

Анализ Def/Ref такое не ловит: снаружи нет ссылки на символ, есть ссылка на
функцию, которая отдаёт его адрес.  Условий для кандидата два, и второе
проверяется только чтением кода — ни один указатель на статику не должен
уходить наружу.

pop_room оба условия проходит (_mobs не читает никто; bake_copy статическая;
atlas_load(&pop_env[i]) берёт адрес глобала из резидентного pop_tile.c).
Остаётся −1620 Б: данные в W2 6774 -> 5157, свободно 129 -> 1746 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:21:44 +03:00
snark13 354582662c Резидент W1/W2: данные pop_room и pop_trob — в свои банки (−2546 Б)
sprinter-cc: новый повторяемый --bank-data=SRC — писучие данные ОДНОГО
банкового модуля в его же страницу.  Прежний --bank-data был всё-или-ничего
и потому неприменим: у большинства банковых модулей часть глобалов читают
соседние банки и резидент (hitp_*, pop_upside, pop_loose_modif, pop_cd), и
такие данные обязаны остаться замапленными всегда.

Кандидаты отобраны по объектным файлам, а не на глаз: символ должен быть
Def только в своём .rel и нигде не Ref.  Прошли ровно двое — pop_room
(_mobs не читает никто) и pop_trob (экспортируемых данных нет вовсе).

При любом --bank-data sprinter-cc сам добавляет mkexe -p 0: crt0 зануляет
только резидентный _DATA, а mkexe по умолчанию бьёт пустоты 0xFF — иначе
вся банковая статика поднялась бы мусором.

Итог: данные в W2 6774 -> 4228, свободно до стека 129 -> 2675 Б.
BANK7 87 %, BANK6 31 %.  Проверено в MAME: уровень 1, комнаты 1-2, фон,
факелы, решётки, проваливающиеся полы, переход между комнатами.

Метод замера и оставшиеся кандидаты — docs/resident_budget.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:14:02 +03:00
snark13 c6828c0ad1 Ворота PoP: правильный гейт слышимости, звук «решётка встала», playsound кнопки
Проверка на сцене 9/9 (кнопка 1,8; челюсти 1,2; ворота — в комнате 4,
тайл 1,9) вскрыла три расхождения.

1. Гейт слышимости стоял как «ворота в текущей комнате», а у оригинала
   (play_door_sound_if_visible, seg007:1239) слышны ещё и ворота в комнате
   СЛЕВА, если они в колонке 9; и НЕ слышны в колонке 9 своей комнаты; плюс
   особый случай «уровень 3, комната 2».  Сцена 9/9 — ровно первый пункт,
   поэтому спуск решётки молчал.  Подъём совпадал, потому что звук открытия
   идёт без гейта — эта асимметрия и была подсказкой.

2. Потерян звук 7 «решётка встала»: gate_stop (seg007:05E3) зовётся из трёх
   мест animate_door и каждый раз играет его через гейт.  У нас во всех трёх
   стояло только type = -1.

3. У trigger_button оригинала есть параметр playsound, нулевой в трёх
   местах (вход на уровень, выход Джаффара, зелье «открыть»).  Добавлен.

Щелчок кнопки слышен через раз — это НЕ баг: prio 0x66 против 0x10 у
челюстей, а укус занимает 465 мс из цикла 1229 мс (замерено).  Разбор с
цифрами — sound_plan.md §12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:03:16 +03:00
snark13 a630568a8b Звук PoP: приоритеты и перебиваемость вместо «всегда перебивать»
Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась
приземлением Кида, в оригинале доигрывает до конца, а приземления не
слышно.  Оказалось, упущен целый механизм.

play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на
кадр — из нескольких выживает важнейший (меньше prio = важнее, при
равенстве последний).  play_next_sound (seg000:1304) раз в кадр решает,
запускать ли: можно, только если ничего не играет ЛИБО текущий помечен
перебиваемым и новый не менее важен.  Иначе номинант выбрасывается —
очереди в оригинале нет.

Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает
целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому
решётка звучит только в паузах между укусами.

Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он
определён безусловно).  Створка двери уровня — единственная запись,
правимая на ходу, вынесена в отдельный байт.  Добавлен пропущенный
stop_sounds на завершении открытия двери (seg007:455).

Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная
на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со
звуком 6.

Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске
после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно
только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:46:04 +03:00
snark13 f159aa47e1 CBL: заливка буфера тишиной при открытии + щелчок на выходе из PoP
Две разные болячки, обе разобраны записью звука MAME в WAV.

libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает
воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то,
что лежало раньше.  Своими данными звук идёт лишь с третьей половины:
прерывание приходит на 128-м слоте и ставит указатель на противоположную
половину.  _cbl_prime заливает буфер тишиной сразу после включения (раньше
нельзя — запись проходит только при поднятом bit7).  В MAME это немо
(эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе
это ровно тот мусор, что ловился на тестовых примерах CBL.

PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один
пропущенный блок (128 сэмплов).  Причина: pop_shutdown освобождал атласы и
графику через ESTEX при открытом звуке, насос не успевал долить.  Звук
гасим первым действием.  Проверено записью — всплеска больше нет.

Заодно записан разбор стартового всплеска (sound_plan.md §10): это не
мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land.  Обрыв
одноголосьем — поведение оригинала (play_digi_sound начинается с
stop_digi, seg009.c:2402).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:23:06 +03:00
snark13 e0c86a96ef Ctrl+S — вкл/выкл звук; чит «выдать меч» убран
Порт 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>
2026-08-20 15:49:33 +03:00
snark13 7e2e7fbb37 Тишина на первом уровне: звук включался только после смены уровня
При разделении загрузки набора (pop_sfx_init) и открытия CBL
(pop_sfx_start) парный вызов start попал только в pop_level_switch.
На стартовом пути его не было — игра шла молча до первого перехода.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 15:25:09 +03:00
snark13 8ea4c32e51 Звуковые эффекты PoP: оцифровка оригинала через CBL
Набор 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>
2026-08-20 15:16:43 +03:00
snark13 24ced6058f Упаковщик звуковых эффектов + сверка наборов MSDOS и SDLPoP
Эффекты берём из MSDOS/digisnd*.dat: 28 из 31 звука совпадают побайтно с
набором SDLPoP, но три различаются не в пользу последнего — у него
sword_vs_sword короче, sword_moving другой, а spiked вообще пустой
(7 сэмплов против 5 069).

MIDI наоборот: у MSDOS формат 0 (всё слито в одну дорожку), у SDLPoP
формат 1 с 8-9 дорожками — готовое разделение голосов, если дойдём до
пути B (MIDI -> три канала AY).

Упаковщик: 31 эффект -> 10 937,5 Гц, 131 072 Б = 8 EMM-страниц.  Начало
каждого звука выровнено на 128 (блок запроса CBL), а страница кратна 128,
поэтому ни один блок не пересекает границу страницы — проигрывателю не
нужна логика стыка.
2026-08-20 12:42:19 +03:00
snark13 a3d37bcbfe Тень закрыта: прогон на уровнях 4/5/6/12, расхождение по кайме в impl_diff 0.0.2-prealpha 2026-08-20 12:33:46 +03:00
snark13 22cdc67c3a Звук: подтверждён формат (8 бит моно) и замерен бюджет EMM
Формат оригинала проверен по convert_digi_sound: один байт на кадр (моно),
байт беззнаковый с центром 0x80 — ровно формат нашего CBL.  Стерео в
данных нет, каналы размножаются на выходе.

Живой замер памяти из работающей программы: занято 124 страницы из 256
(система с exe 43, наши ассеты 81), свободно 132 = 2,06 МБ.  Самый
крупный ассет теперь набор Тени — 32 страницы.  Эффекты WAV займут 8.
2026-08-20 12:25:31 +03:00
snark13 4688364091 Звук: решения пользователя и единая частота 10 937,5 Гц
Эффекты — WAV через CBL; музыка первым заходом путь A (ноты на AY);
заставки потом WAV; музыка по ходу игры — открыто (WAV с гашением
эффектов либо путь B).

Единую частоту берём не 11 000, а ровно частоту CBL 10 937,5: тогда тон
точен (иначе −9,9 цента), а пересчитывать три файла всё равно надо.
112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц; рост целиком от
leveldoor_sliding (источник 2 750 Гц).  Взамен CBL открывается один раз и
частота не меняется никогда.
2026-08-20 12:10:04 +03:00
snark13 9f9a8f26ae Звук: разобраны все наборы MS-DOS версии, включая mt32snd
Эффекты все 31 есть в WAV (digisnd, 8 бит PCM) — просьба «не спикер, а
wav» для них уже выполнена исходным планом.  mt32snd оказался НЕ музыкой,
а теми же эффектами в MIDI для Roland MT-32.

У музыки WAV нет ни в одном наборе: только ноты спикера (7 КБ) или MIDI
(27 КБ).  Посчитал третий путь — рендер MIDI в WAV на хосте: игровые
треки 74,7 с = 803 КБ = 50 EMM-страниц (влезает), заставки 248 с = 167
страниц (только стрим с диска).

Только спикером во всей игре остаётся один звук — blink (4 ноты).
2026-08-20 11:57:19 +03:00
snark13 7fcd93a35b Разбор звука: эффекты через CBL как есть, музыка на AY из нот PC-спикера
Замеры по ассетам: 31 эффект уже 8-битным PCM на 11 000 Гц (у CBL есть
10 937,5 — расхождение 0,6 %, формат сэмпла совпадает байт в байт, то
есть конверсии нет вовсе), 103 941 Б = 6,3 EMM-страницы.

Вся музыка есть нотами PC-спикера — 7 КБ на 57 звуков, и нота там задана
в ГЕРЦАХ напрямую (проверено по speaker_callback), а не делителем PIT,
как кажется по числам.  MIDI разбирать не нужно.

Отдельный таймер не нужен: секвенсор музыки двигает CBL-callback раз в
11,7 мс, а короче 12 мс во всей музыке 2 ноты из 1469.
2026-08-20 11:53:02 +03:00
snark13 d1183f7315 Отладочный старт перехватывал смену уровня
DBG_START_ROOM/POS подменялись безусловно, а pop_start_level зовётся и на
границе уровня.  Из-за этого на 7-м стартовой становилась отладочная
комната вместо комнаты 17 из данных, и спецсобытие «вход падением»
(set_start_pos, seg003:0196) не срабатывало — переход 6->7 выглядел
сломанным.

Подмена теперь действует только на своём уровне (FIRST_LEVEL); рестарт
того же уровня отладочную позицию сохраняет, как и задумано.
2026-08-20 11:35:09 +03:00
snark13 30bcc3459b Атлас Тени: запечённый набор вместо спрайтов стража
Оригинал кладёт спрайт дважды — прозрачным блитом в x и XOR-блиттером в
x+1; пакетный блит так не умеет, поэтому результат запечён упаковщиком.
Две половины, как и у оригинала: sk* — кадры вне боя (спрайты Кида),
sf* — кадры 150..189 (SHADOW.DAT, тоже графика Кида).  251 спрайт,
32 EMM-страницы, палитра 16 цветов в 0xA0..0xAF.

Закрывает BUG-SHADOW-SET: раньше тип 4 уходил в guard_names, и Тень в
бою дралась серым стражем.

Грабля: kid.pal заливает все 256 записей и затирает слоты Тени —
палитра вынесена в pop_shadow_pal_apply рядом с pop_bg_pal_apply.

Проверено в MAME на 6-м уровне: силуэт с контуром, как в оригинале.
2026-08-20 11:22:53 +03:00
snark13 e60a04e900 Тень: набор спрайтов выбирает поле кадра, а не charid; SHADOW.DAT у нас нет
Поправка к вчерашнему выводу «в бою Тень рисуется спрайтами стража».
Набор берётся из cur_frame.sword>>6 (seg008.c:1752), chtab_base жёстко
равен Киду.  Тень идёт через chtab_5, но chtab_5 — это «соперник уровня»,
и на 12-м это SHADOW.DAT: графика КИДА в боевых позах, палитра побайтно
равна палитре Кида.  Пользователь прав — Тень всегда выглядит Кидом.

Наш pop_guard_load уводит тип 4 в guard_names, SHADOW.DAT в ассетах нет
вообще — заведён BUG-SHADOW-SET.

Пересчитал палитру на правильных наборах: 251 кадр, 45 цветов; 16 цветов
гибридом дают 76 грубых промахов на все кадры (было 129 на ошибочном
наборе).
2026-08-20 10:50:36 +03:00
snark13 b6660d7694 Атлас Тени: остановились на 16 цветах (гибридный подбор), блок 0xA0..0xAF 2026-08-20 10:39:40 +03:00
snark13 acb483897d Разбор атласа Тени: алгоритм оригинала, замеры палитры, план
XOR у оригинала идёт по 24-битному RGB, а blitters_2_or — обычный блит с
colour key 0.  От фона зависит только кайма в один пиксель по левым
кромкам силуэта; на чёрном фоне запечка точна.

Замеры: 253 кадра (Кид 219 + страж 34, Тень в боевых кадрах рисуется
спрайтами СТРАЖА), 59 разных цветов.  32 цвета оставляют перцептивно
значимыми 232 пикселя из 96 746.  Палитра: занято 112 слотов, свободно
144; берём 0xA0..0xBF.
2026-08-20 10:38:27 +03:00
snark13 47c26d1899 TUNE-2: параметры стражей в CFG-файл (формат секций как у SDLPoP) 2026-08-20 10:23:20 +03:00
snark13 0ef8c4b60e Бессмертие: два уровня вместо тумблера
1 — только бой: удары мечом не отнимают HP (ветка в hurt_by_sword).
2 — плюс мелкий урон: не проходят «минус деление» от падения с двух
    этажей и от падающей плиты (pop_take_hp гасит count < 100).
Мгновенная смерть остаётся на обоих: пики, чомпер, падение с трёх этажей
и удар вне боевой стойки приходят с count = 100.  В коде ровно два
значения урона, 1 и 100, поэтому граница точная, а не эвристическая.

Заодно ушёл костыль «снять бессмертие на время вызова take_hp(100)» в
hurt_by_sword — он был нужен только потому, что прежний чит глушил и
смертельный урон.

Клавиша I идёт по кругу 0 -> 1 -> 2 -> 0; в отладочной метке число
красных палочек = уровень.
2026-08-20 10:08:52 +03:00
snark13 10b920f156 impl_diff: ГСЧ разведён по доменам (у оригинала один сид) 2026-08-20 09:43:07 +03:00
snark13 6bdac70508 Отладочная метка: уровень, комната, режим скорости, бессмертие; дефолт NORMAL
Четыре блока палочками в верхнем борте, каждый своим цветом.  Цвета взяты
из 0x3A..0x3F — единственного диапазона, который не перезаписывают ни
зелья (0x40), ни env/wall тайлсета (0x50/0x60), ни страж (0x90).  Прежняя
метка комнаты рисовалась цветом 0x57, то есть из env-диапазона: белой она
была только в подземелье, во дворце брала цвет тайлсета.

Записи палитры — BGR (BIOS $A4), не RGB; читать kid.pal «как привычно»
нельзя, цвета выйдут переставленными.

Режимы перенумерованы: NORMAL=0, FAST=1, FASTEST=2.  Тогда дефолт (crt0
зануляет _DATA) — NORMAL, обход инкрементом даёт NORMAL->FAST->FASTEST, а
номер режима + 1 = число палочек.
2026-08-20 09:40:57 +03:00
snark13 5d61224229 Реестр оптимизации: бюджет кадра вырос втрое, срочность позиций падает 2026-08-19 23:23:22 +03:00
snark13 35d7bd38d4 L1-SPEED закрыта режимами скорости 2026-08-19 23:21:52 +03:00
snark13 5e9c6a2e9e Пейсинг: результаты замеров и грабли методики 2026-08-19 23:21:33 +03:00
snark13 a6e39070af Фиксированный логический кадр по лучу + режимы FASTEST/FAST/NORMAL
Период стал max(n, ceil(W)) вместо ceil(W)+2: три gfx_wait_vsync после
работы отсчитывались от её КОНЦА, поэтому бюджет кадра был один растр.
Теперь ждём от якоря начала кадра, и при n=3 бюджет 1 290 240 тактов.

Счёт кадров — программный, по биту 5 порта 0xFE (положение луча), а не по
кадровым прерываниям: те теряются в DI-окнах акселератора фазозависимо
(замер: 0..2,8 %, на полной перерисовке три подряд).  Условие точности
одно — зазор между выборками меньше 64 512 тактов; точки выборки
расставлены по замеру, а не на глаз.

Режимы (pop_pace.h), клавиша P по кругу, дефолт FASTEST.  Условие боя
взято у оригинала буквально (SDLPoP seg003.c:363): Kid.sword ==
SWORD_2_DRAWN, а не «идёт бой».

Проверено в MAME на 11/15: счётчик без недосчёта на 270 кадров, период
ровно 3 растра на 302 логических кадрах (ни длиннее 3,1, ни короче 2,9),
NORMAL даёт ровно 4, с вынутым мечом — ровно 5.
2026-08-19 23:06:27 +03:00
snark13 61b8d80275 Пейсинг: подробный разбор счётчика кадров по лучу (условие точности, точки выборки, приёмка) 2026-08-19 21:55:05 +03:00
snark13 7f778bba2f Разбор перехода на фиксированный логический кадр (кода не трогали)
Замеры в MAME: кадровые прерывания теряются фазозависимо (0..3%), на
полной перерисовке комнаты — три подряд.  Причина: импульс запроса 32
такта (9,14 мкс) против DI-окон блита ~0,29 мс.  Счёт попаданий
брейкпоинтом на этом драйвере недостоверен (WAIT-линия), достоверен
только детектор разрыва.

Блокер включения gfx_set_fps_div как есть: счётчиковый путь ждёт через
halt и не зовёт idle-хук, то есть возвращает KBD-1.
2026-08-19 21:41:13 +03:00
snark13 39c3247532 Док 13/23: регресс после дня оптимизации 11/15
Максимумы по секциям против эталона mob-order-B-done: работа 911 862
(-1 986), синяя 149 106 (-10 704), зелёная 436 494 (-3 924), циан 382 770
(-10 230).  Период 3 растра в 2341 кадре, 4 в 23, 5 в 2 — как в эталоне.

Записано, почему сумма минусов по фазам не равна минусу по работе:
максимумы разных фаз достигаются в разных кадрах, а «работа» — максимум
суммы, а не сумма максимумов (вопрос пользователя).

Отмечено, что зелёная по-прежнему выше растрового кадра и главный
оставшийся кандидат для этой сцены — P9 (G8): сосед падающей плиты
перезапекается целиком и повторно, при шести плитах это умножается.

И записан урок процесса: прогон 13/23 обязателен после каждой правки
loose-механики — именно он вскрыл пропущенный взвод гейта в check_fall_flo,
которого не поймали ни хост-тесты, ни сцена 11/15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.0.1-prealpha
2026-08-19 18:56:07 +03:00
snark13 d0ac4b1c2a Каскад уровня 13 не падал: в гейте P5 пропущено шестое место взвода
Нашёл пользователь на прогоне 13/23: плиты потолка трясутся, но не
падают.

Причина — моя ошибка в P5.  Гейт loose_any снимается циклом по факту
прохода без живых фаз, а взводиться обязан у КАЖДОЙ записи фазы.  Я
пометил пять мест и пропустил шестое: check_fall_flo, который на уровне 13
раздаёт плитам-потолкам отложенный старт (0xF0..0xFF).  В результате фаза
записывалась, а цикл её не досчитывал — ровно тот отказ, который я сам
описал в комментарии к loose_any: «ложный ноль стоит застывшей навсегда
плиты».

Исправлено, и в шапку loose_any добавлено предупреждение с этим случаем:
добавляя новое место записи фазы, добавляй и взвод.

Замер 13/23 после исправления (максимумы по секциям, 2367 кадров):

                эталон    сейчас
  работа       913 848   911 862
  синяя        159 810   149 106
  зелёная      440 418   436 494
  циан         393 000   382 770

Период: 3 растра в 2341 кадре, 4 в 23, 5 в 2 — как в эталоне.  Зелёная
по-прежнему выше растрового кадра (436 494 против 430 000).

Урок для процесса: сцену 13/23 надо прогонять после КАЖДОЙ правки
loose-механики, а не только когда меняешь её сознательно.  Хост-тесты
этот отказ не поймали: phys_loose_gate_survives_room_change проверяет
возврат в комнату, а не отложенный старт уровня 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:50:32 +03:00
snark13 d0de6dedf4 HEAL-WIDTH: heal чомпера ровно 32x60 — его точный след
Замечание пользователя: весь чомпер помещается в свой тайл, значит его
heal максимум 32x60.  Проверено по каталогу атласа и подтвердилось:

  нижняя челюсть 101/102 = 32x60 низом на dmy = 63*row+62, занимает
    63*row+3 .. +62;
  верхние челюсти дают ТОТ ЖЕ верх — подъём 0x25 при высоте 23, 0x2F при
    13 и 0x32 при 10 все три упираются в 63*row+3;
  кровь 114..118 шириной 6 рисуется на x+8, то есть внутри 32.

Было 64 «на всю высоту тайла» (плюс лишняя строка запаса от прошлой
правки) — стало ровно 60 от +3.

Заодно зафиксирован разбор структуры перерисовки чомпера: ОДИН heal на
тайл и ДВА блита (низ и верх).  Объединить блиты нельзя — при раскрытых
позах нижняя часть маленькая (32x30, 32x21, 32x17) и между ней и верхней
челюстью разрыв: например, при позе 2 низ занимает +33..+62, верх
+3..+25, а строки +26..+32 пустые.

Проверено в MAME: чомпер рисуется чисто, хвостов от прежнего кадра нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:36:04 +03:00
snark13 b6b214e225 Реестр: HEAL-WIDTH закрыт, P10 разобран без реализации
HEAL-WIDTH: плита 64->58, чомпер 64->61, габариты из каталогов атласов.
На 11/15 медиана не сдвинулась (плит нет), максимум -960.  Основной
эффект ждёт прогона 13/23, где плит шесть одновременно.

P10 разобран: «просто передать готовое из физики» не выйдет, величины
РАЗНЫЕ.  char_footprint берёт габарит кадра и расширяет диапазон на
колонку под меч; set_char_collision тот же fpw корректирует на FRAME_THIN
и меч не учитывает, а ряды у него — опорный curr_row, а не верх/низ
спрайта.  У оригинала обе задачи пользуются одними величинами, потому что
он считает их один раз; у нас они исторически разошлись.

Значит P10 — это сведение двух геометрий к одной, с риском для физики,
которая сейчас работает правильно.  Приоритет понижен до низкого, и
записано, чего не хватает: отдельного замера самого char_footprint
(сейчас известно лишь «вход + set_clip + footprint = 10 872»).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:27:18 +03:00
snark13 f62989e358 HEAL-WIDTH: ширина heal'ов по фактическому следу из атласа
Задача была помечена обязательной.  Габариты сняты из каталогов .atl, а
не «по клеткам на глаз»:

  плита   41/69/70 = 32x13-14   43/73/74 = 32x3   42/71/72 = 26x15-16
  чомпер  101/102 = 32x60       111 = 27x23       113 = 23x10

Отсюда два сужения:

  pop_loose_shake_draw  ширина 64 -> 58  (свой тайл 32 + правая грань 26,
                                          во дворце 25)
  pop_chomp_redraw      высота 64 -> 61  (след 63*row+3..62: верх самого
                                          высокого bot-кадра и низ на dmy;
                                          верхняя челюсть при подъёме 0x32
                                          и высоте 10 даёт ровно +3)

Замер 11/15: медиана не сдвинулась (437 484 — плит в комнате нет),
максимум 550 776 -> 549 816, то есть эффект только в кадрах перерисовки
чомпера и он мал, как и предсказал пользователь.  Основной выигрыш от
сужения плиты (9,4 % площади) ждёт сцены 13/23 и требует отдельного
прогона на сборке LEVEL=13.

Пики не трогал: их таблицы кадров (POP_SPIKES_FRAM_LEFT/RIGHT) я по
атласу не разбирал, а сужать heal по догадке — прямой путь к
недочищенному хвосту.

Проверено в MAME: чомпер и факелы рисуются чисто, хвостов нет; хост-тесты
зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:25:35 +03:00
snark13 a29fb8da34 Реестр: P6a закрыт (-840), P6b оставлен неделанным
Оценка пары P6a/P6b была -20 000, факт по P6a — -840.  Записана причина:
оценку я перенёс по аналогии с лучом видимости, где трамплин звался девять
раз за кадр, а тут trob'ов в комнате всего несколько.  Урок в реестре:
«тот же паттерн» не означает «тот же порядок величины».

P6b (кэш префетча, 11 058) не делался: инвалидацию пришлось бы ловить из
трёх источников (pop_level_set_tile, вход в комнату, добавление trob), а
пропуск любого даёт застывшую анимацию.

Бюджет лёгкой позиции: 437 484, до цели 7 484.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:18:13 +03:00
snark13 5a42b2d245 P6a: указатель модификаторов комнаты кэшируется между trob'ами
pop_trob_modif объявлен __banked, а звался он на КАЖДЫЙ trob внутри
цикла pop_process_trobs — при том что комната у них в подавляющем
большинстве кадров одна (чужие появляются только у брошенных плит
соседней комнаты).  Тот же паттерн «трамплин в цикле», что дал -23 784 на
луче видимости (P2b) и -14 118 на guard_over_kid (P16).

Замер 11/15: цикл trobs 78 726 -> 74 964, работа кадра 438 324 -> 437 484.

ОЖИДАНИЕ НЕ ОПРАВДАЛОСЬ: в реестре стояло -20 000 на пару P6a/P6b, а
вышло -840.  Причина простая — trob'ов в комнате всего несколько, и кэш
экономит два-три вызова, а не двадцать.  Оценка была построена на
аналогии с лучом видимости, где вызовов было девять на КАЖДЫЙ кадр.

Проверено в MAME на чистом запуске: факелы, чомпер и страж рисуются
правильно, хост-тесты зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:17:38 +03:00
snark13 52a36caa75 Реестр: P18 — метка огрублена по X (отложено)
Пользователь заметил: Кид перерисовывается, хотя с пламенем не
пересекается; на пиксель левее — перестаёт.

Разбор: спрайт Кида занимает x 213..224, колонка считается как x >> 5,
и 224 — ровно первый пиксель колонки 7, где лежит метка от пламени
(y 33..50).  По вертикали пересечение настоящее, по горизонтали его нет:
пламя в той же колонке занимает x 232..247, зазор восемь пикселей.

То есть P15 исправил огрубление по Y и оставил его по X.

Отложено по решению пользователя с его же аргументами: x не влезает в
байт (0..319), значит нужны 16-битные сравнения в горячем пути, а они у
SDCC z80 дороги настолько, что могут съесть выигрыш; огрубление вдвое —
лишний сдвиг при записи и проверке плюс потеря точности.

Записана непроверенная идея: хранить границы как смещение ВНУТРИ колонки
(0..31, пять бит) — байта хватит и сравнение 8-битное, но запись
усложняется для прямоугольников через несколько колонок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:07:36 +03:00
snark13 9e03739bb0 Реестр: P4 откачен, добавлен P17 (разрядность)
P4 (каталог из W0) отменён по критерию пользователя: 408 тактов не стоят
второй публичной функции в libbgi с неявным контрактом «страница уже
подключена».  Знание сохранено: gfx_w0_map стоит 324, поэтому потолок
непробованной части P4 — около 2 600, а не 10 000.

P17 — по замечанию пользователя про 16 бит там, где хватает 8: границы
экрана беззнаковыми сравнениями (-378) и габариты спрайтов в uint8_t
(-276 в статике, -1 134 в циане динамики, плюс 24 байта _DATA).

Бюджет: лёгкая 438 324, тяжёлая 603 684.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:02:09 +03:00
snark13 79bcabde94 Габариты спрайтов в байтах: uint16_t -> uint8_t в слоте отрисовки
Замечание пользователя: спрайты наших атласов не крупнее 64x64, а w/h
почти везде были uint16_t.  Это уже записано в памяти проекта
(pop_sprite_size_limits: весь игровой кадр PoP <= 56x63; больше 255 только
восемь полноэкранных подложек титров, а они через слот персонажа не
проходят).

Переведены в uint8_t: w/h, ow/oh, fpw/fph, cw/ch в pop_cdraw_t, параметры
cd_overlay_add и cd_clip_add, локали w/h/vis_w в pop_char_draw и
cd_splash, и чтение габарита из шапки ленты (было двухбайтным сложением
со сдвигом).

Эффект: лёгкая позиция 438 600 -> 438 324 (там персонажи не рисуются,
поэтому почти ничего), тяжёлая — циан 181 404 -> 180 270.  Плюс 24 байта
_DATA на двух слотах.

Скромно, но код от этого не запутаннее, а честнее: тип теперь отражает
реальный диапазон.  Проверено в MAME — бой идёт, хвостов и обрезков нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:00:19 +03:00
snark13 892f005ca5 pop_blit_b: границы экрана двумя беззнаковыми сравнениями вместо четырёх знаковых
Проверка «спрайт целиком на экране» стояла как
  pb_x >= 0 && pb_top >= 0 && pb_x + pb_w <= 320 && pb_top + pb_h <= 256
— четыре знаковых 16-битных сравнения, а знаковое у SDCC z80 разворачивается
в пару sbc плюс jp PO / xor 0x80 / jp P (видно в листинге).

Беззнаковая форма делает то же двумя: отрицательная координата в
беззнаковом виде становится очень большой и проваливает условие так же,
как проверка >= 0, а верхняя граница переносится в правую часть вместе со
сложением.  Границы неотрицательны по построению: pb_w и pb_h не больше
255, значит 320-pb_w >= 65 и 256-pb_h >= 1.

Работа кадра 438 978 -> 438 600.  Немного, но идиома стандартная и код
не усложняется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:52:43 +03:00