Уровень 9 (зелье инверсии) — багов не найдено.
Регресс 13/23, 2701 кадр: работа 880 272, синяя 159 822, зелёная 419 562,
циан 380 202 — против 880 170 / 159 774 / 419 520 / 380 244 у 3bcaf51.
Разброс ±100 тактов на 880 000 (0,01 %), циан даже в минус. Период совпал
кадр в кадр: 4 растра в 24 кадрах, 5 в одном. Host-тесты 5106 проверок
без расхождений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2435 кадров сцены каскада. Максимумы по секциям: работа 880 170,
синяя 159 774, зелёная 419 520, циан 380 244 — против 878 550 / 158 880 /
419 526 / 379 488 у ec1f384. Все четыре в пределах шума прогона, зелёная
совпала до 6 тактов. Распределение периода совпало кадр в кадр: 4 растра
в 24 кадрах, 5 в одном, остальные 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XOR-приём оригинала требует чтения видео-ОЗУ (нельзя) и несовместим с
0xFF-прозрачностью; план — отдельный атлас тени.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пересмотр по вопросу пользователя. Первая редакция плана рекомендовала
EMM-страницу — ошибка: взвешивала скорость и недооценивала главный
сценарий.
EMM-страница не переживает рестарт программы, а именно рестарт — тот
случай, ради которого QuickSave и нужен: сцену каскада плит на 13/23
воспроизводит ТОЛЬКО ESC → запуск заново (perf_l13_room23.md §1). Снимок
в ОЗУ там не помогает вовсе.
Доводы за EMM при перепроверке оказались слабыми: лимит манипуляторов DSS
ни при чём (один файл, гард _fd_guard и так стоит), а экономия на пути к
файлу — одна строка. Разница в скорости некритична: 1,9 КБ на HDD не
заметны на фоне полной перерисовки комнаты при загрузке.
Добавлен шаг QS0 — проверить, что D: вообще пишется из-под MAME: если
образ только на чтение, это меняет весь план, поэтому идёт первым.
Критерий приёмки задачи: сохранить, выйти, запустить заново, загрузить —
и оказаться там же.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Только изучение и план, кода нет.
Первое, что выяснилось: в оригинале 1989 года быстрого сохранения НЕТ
вовсе — это enhancement SDLPoP (seg000.c, USE_QUICKSAVE, F6/F9). Значит
искать в Apple II / MSDOS нечего, и повторяем мы не букву, а устройство.
Что берём у SDLPoP: плоский снимок с ОДНИМ обходом на запись и на чтение
(#define process(x)); совместимость держится строкой версии и ничем больше;
клавиша только взводит флаг, работа идёт между кадрами; состояние отрисовки
не сохраняется вовсе — комната перерисовывается с нуля.
Чем наш случай тяжелее: уровень в EMM-странице, room_modif/trobs — static в
банковом pop_trob.c, ГСЧ у нас ТРИ (pop_t_seed, trob_seed, pop_fight_seed),
и дабл-буфер требует перерисовать после загрузки ОБЕ страницы.
Снимок ≈1,9 КБ, поэтому основной носитель — EMM-страница (мгновенно, мимо
DSS и его лимита манипуляторов), файл вынесен в необязательный шаг QS6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).
работа синяя зелёная циан
af189a1 916 458 142 830 546 900 270 510
40f0d46 873 930 158 874 417 630 379 482
ec1f384 878 550 158 880 419 526 379 488
Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона. Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.
Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000. Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Идея пользователя 2026-08-17. При падении плиты помечаются ДВА тайла, и
сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО (draw_tile для него зовётся дважды),
хотя потревожили у него только левые 28 px — там, куда свисает правая грань
упавшего тайла.
В записи разведено, что 60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес (сужать
нельзя), и сузить можно только пометку «изменился мой ЛЕВЫЙ сосед». Плюс три
условия: pop_floor_bake общая (кнопка/зеркало/предмет/щебень) и нужен
отдельный вход; вертикальные диапазоны двух запечек НЕ совпадают (20 строк
против 39), поэтому просто снять вторую пометку нельзя; ширину полосы брать
по максимальному свесу.
Брать ПОСЛЕ обхода всех уровней — решение пользователя.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новый раздел §4: копия годится только если (1) запечка ограничена
копируемым прямоугольником — иначе то, что легло вне него, на второй
странице остаётся прежним и страницы расходятся (мерцание торцов, ур.1
к.6, кнопка (0,2)); и (2) содержимое тайла между двумя кадрами не
менялось — у кнопки с идущим таймером пометка обновляется каждый кадр.
Перечёркнут прежний вывод «окно клипа в pop_floor_bake — только вред»:
по скорости да, но оно ОБЯЗАТЕЛЬНО как условие (1).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зелёная и циан — раскладка ПОСЛЕ правок, журнал правок с коммитами,
отрицательные результаты с объяснением причины (окно клипа в
pop_floor_bake — три попытки), и что осталось: раскол draw_tile на девять
узких частей по образцу seg008:01C7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок). Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.
Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх. По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.
Что нашлось (всё подтверждено зондами, не гипотезы):
RDA_CEIL дрожащая плита-потолок 48 658 x до 6 = 292 000
RDA_CEIL_GONE запечь колодец 251 023 x до 2 = 619 000
RD_FLOOR щебень на месте посадки 198 259 x до 2 = 397 000
Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600. У blit_b_clip — 22 Б кадра и 211 (ix).
Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.
Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.
Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60. Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).
Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):
docs/perf_l13_room23.md сцена, рецепт воспроизведения, зонды, канал clog,
сводка по кадрам, габариты спрайтов
docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
docs/perf_cyan_phase.md циан: раскладка, позиции C1..C7, журнал
Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest). Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_redraw_fore (seg007:0550) зовут ровно из трёх мест, и нигде нет
пометки «всё»: персонаж помечает прямоугольник своего футпринта
(redraw_at_char, seg003:0576; для Кида — объединение с прошлым кадром),
падающий кусок — соседа справа и второй тайл на границе рядов (draw_mob,
seg007:1147), анимация тайла — один тайл.
Значит пометка переднего слоя НЕ ШИРЕ нашего окна вокруг персонажа, и
полный порт objtable её не отнимает, а формализует. Главный риск снят.
Побочно найден точный рецепт для MOB-CLIP-RIGHT: оригинал не рисует соседа
поверх куска и не полагается на clip.right — он помечает соседний тайл
парой set_redraw2 + set_redraw_fore, и порядок делает всё сам. Это же
закрывает «плиту перед колонной».
Рекомендация по итогам: вариант A (полный порт).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 1 показал ошибку критерия: комнаты 13 и 18 ссылаются только друг на
друга (13.down = 18, 18.up = 13), то есть входящая связь у каждой есть, но
из остального уровня в них не попасть. Счётчик входящих их пропускал.
Заменено на BFS от стартовой комнаты по связям. Уровень 1 теперь даёт
ровно {13, 18, 24}, как и должно быть. Заодно всплыло, насколько мало
комнат реально проходимо на поздних уровнях: 13-й — 11 из 24, 14-й — 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Посчитано скриптом по res20NN.bin для всех 15 уровней. Три критерия:
нет входов (ни одна комната не ссылается связями), нет пола (стоять не на
чем) и пол только опасный (обычного пола код 1 нет — пики/расшатанные).
Плюс спецкомнаты, которые пропускать независимо от критериев: seamless
exit 12/23 (меняет уровень), falling exit 6/1, falling entry 7/17,
спуск-через-зацеп 7/14, тень с зельем 5/24 и 13/23+13/16, где вход роняет
гряду плит. Уровень 15 (защита от копирования) — целиком.
Нужна для полного обхода комнат с первого уровня после порта слоёв.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спрайты: у падающего куска obj_id = 10 (add_mob_to_objtable, seg007:1170),
и три части берутся из таблиц по этому индексу — 70/74/72, а не 41/43/42.
Мы рисовали плиту В ПОКОЕ, отсюда «цельная ровная» вместо двух половин со
сдвигом правой на пиксель.
Подпорка «перерисовать соседний тайл поверх куска» убрана: она тащила на
плиту чужой узор и окно, да ещё стоила по одной полной отрисовке тайла на
кусок за кадр. Клип объекта (clip.right = 40) НЕ портирован — единицы поля
не выяснены, буквальные 40 пикселей срезают задний угол плиты. Цена отказа
— правая грань куска и передние части чужих тайлов его не перекрывают
(MOB-CLIP-RIGHT, BUGS_OPEN.md).
Коридор heal сужен по высоте 32 -> 24: реальный след спрайта 16 px.
Ширина оставлена 64 — из неё используются 62, экономить нечего; заодно в
комментарии зафиксировано, что «выравнивание блока акселератора» ничем не
подтверждается и требует проверки артефактом.
docs/midtable_analysis.md — разбор слоёв оригинала и замеры:
- логический кадр 1 289 526 тактов;
- pop_redraw_needed 978 тактов (0,08 %);
- fore-проход 128 778 тактов (10 %) на ОДНОГО персонажа, но выполняется
лишь на 4 % кадров — гасит пропуск неизменившегося персонажа;
- максимум объектов на одном тайле 2 (пара из loose_fall), значит
сортировка внутри тайла бесплатна.
Вывод: единственный риск порта objtable — сохранится ли окно клипа
fore-прохода; до разбора redraw_frames_fore выбирать вариант рано.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 12 (тень) — порт seg002/seg006:
- check_shadow: подъём тени в комнате 15 по условию «меч подобран»,
init_shad_12, вход падением (seq 7);
- autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ;
- общий урон (ранил тень — ранил себя) и check_killed_shadow
(убил тень — убил себя);
- таймер вспышки слияния 42 -> -1, sword_disappears при уходе из
комнаты 18, появление плит в комнатах 2/13 после слияния.
Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом
попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт
сбросить HP. Чит навигации по комнатам этот триггер придерживает —
иначе комнату 23 двенадцатого уровня не посмотреть в принципе.
Уровень 13 (Джафар): guard_notice_timer (фора после встречи),
on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три
исключения loose-механики. Плюс спрайты визиря VIZIER.DAT — без них он
рисовался обычным стражем; заодно цвет из данных комнаты применяется
только к обычному стражу, как в оригинале.
Падающие плиты — семь дефектов, найденных прогонами:
- MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние
терялись без щебня;
- одиночные сигналы посадки/провала больше не затираются в кадре;
- честный loose_fall: сбитая плита рождается в том же кадре, от места
удара, с половинной скоростью;
- при снятии плиты метится и сосед справа (висел передний торец);
- heal и отрисовка кусков разнесены на два прохода;
- куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y
(compare_curr_objs для пары 0x80 сортирует наоборот);
- wall_pattern убран из ceil_over_kid_tile: у оригинала узор из
draw_tile_bottom идёт в фон, а не в передний слой.
Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44).
TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам.
Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами
тени при слиянии, чит навигации не запускает бесшовный переход, чит K
убивает через штатный путь смерти.
Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER,
GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три правки по следам прогона пользователя на зелье инверсии.
1. ЛИШНИЕ ЯЗЫКИ ПЛАМЕНИ. pop_flip_screen отражал уже нарисованное копией
акселератора, а пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию (pop_torch_draw: heal'а
у него нет, следующий кадр непрозрачно накрывает предыдущий). Отражённое
вместе с фоном, оно оказывалось в зеркальной позиции, где накрывать его
некому — и оставалось навсегда (уходило только после входа в комнату,
который строит фон с нуля). Теперь переворот ПЕРЕРИСОВЫВАЕТ комнату тем же
приёмом, что вход (ENTER-ROOM-FAST). Это совпадает с оригиналом: SDLPoP на
need_redraw_because_flipped зовёт redraw_screen(0), а не отражает картинку
(seg000.c:928).
2. МОМЕНТ ПЕРЕКЛЮЧЕНИЯ. Зелье выпивается из play_seq, в середине кадра, и
pop_upside переключался прямо там — остаток кадра рисовался зеркально
поверх ещё неперевёрнутого фона. Разделены pop_upside_want (пишут зелье,
смерть, чит U) и pop_upside (читают слои); переключение — одно место, начало
кадра, вместе с перерисовкой. Оригиналу этого не нужно: он всегда рисует в
offscreen неперевёрнутым и зеркалит только на выводе — расхождение записано
в docs/impl_diff.md.
3. CLIP_CHAR В ПЕРЕВОРОТЕ. Граница clip_char приходит в ЛОГИЧЕСКИХ
координатах (y_clip), сравнивать её с уже отражённым top нельзя: то, что
логически выше линии, на экране ниже неё. Теперь тот же срез применяется
с другого конца — укорачивает кадр снизу (bcut), верх остаётся на месте;
строки ленты не сдвигаются, потому что зеркальная лента отдаёт их в
обратном порядке. Раньше в перевороте клип был отключён совсем, и висящий
Кид рисовался поверх плиты.
Проверено в MAME: переворот чистый, фон без остатков. tests-host 6/6.
Найдено пользователем: после телепорта в перевёрнутом виде ниже поля мусор.
Спрайты, торчащие в обычном виде ВЫШЕ поля (полоса кладки у потолка), после
отражения торчат НИЖЕ и лезут в борт с полосой HP.
blit_b_clip при перевороте режет по обеим границам поля всегда, а не по
pop_t_clip_top; быстрый путь pop_blit_b отдаёт клипующему всё, что выходит
за поле.
Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
комнаты (факелы, чомперы, пол) зеркальны. Переходы между комнатами в
перевёрнутом виде работают.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
II.3: pop_upside проведён в единственную точку тайлового слоя (pop_blit_b /
blit_b_clip): позиция через FLIP_TOP, блит — vflip-двойник. Этим зеркалятся
полная отрисовка комнаты, точечные редрои, trob-анимации, fore-слой и кладка.
В клипованном пути обрезка сверху экрана съедает нижние строки источника,
поэтому кусок пересчитывается как sy' = h - sy - dh.
MEM-COLD2 п.1: enter_room_side/enter_room — в банк 8 (куча 450 -> 1231 Б).
Рабочие массивы комнаты остались в _DATA, в банк уехал только код.
Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком, пол
вверху, дверь и кладка вверх ногами. tests-host 6/6, size-check OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вход в комнату: одна отрисовка в скрытую страницу, показ флипом, вторая
страница — gfx_copy_page(DIRECT) вместо повторной отрисовки. Процесс
рисования тайлов больше не виден, обе страницы синхронны (два снимка подряд
идентичны). Видимую страницу главный цикл теперь перечитывает у железа в
начале кадра: её меняют и вход в комнату, и переворот — оба из банка.
MEM-COLD2 п.2: guard_over_kid с хелперами (objtile_at_char/char_x_left_of/
tile_div_mod) — в банк 8. Куча 1005 -> 1574 Б, цена — один трамплин на кадр.
tests-host 6/6, size-check OK, в MAME проверены старт уровня 9, переходы
между комнатами и отрисовка стража.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шаг II.1 плана. pop_upside/pop_upside_dirty (порт upside_down и
need_redraw_because_flipped), ветка case 4 в proc_get_object (только тоггл —
ни вспышки, ни урона, как в оригинале), сброс стартом уровня и смертью Кида.
pop_flip_screen (банк 8) переворачивает уже нарисованное двумя копиями
акселератора: VFLIP в скрытую страницу, показ, DIRECT во вторую; затем
инвалидация слотов персонажей, метки фона и полосы HP.
Чит-клавиша U — порт seg000:0793 (в оригинале переворот тоже на чите): без
неё эффект не проверить, чит-телепорт до зелья в комнате 7 не дотягивается.
Проверено в MAME на уровне 9: комната переворачивается целиком за кадр,
повторное нажатие возвращает. Персонажи и факелы пока не зеркалятся —
это шаги II.3/II.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первый шаг L9-INVERT. Разведка железа закрыта экспериментом: Port_Y можно
менять между read- и write-триггером, если перед вторым OUT стоит STOP —
приём уже был в проде в _bgi_scroll_cols_raw (указал пользователь), так что
обходной путь через ОЗУ-буфер не понадобился.
- _bgi_flip_rows_raw: построчная копия video->video с реверсом Port_Y
приёмника (DI/EI бандами по 16 строк, размер блока армируется в каждой
скобке, убывающий y1 держится SMC-ячейкой);
- gfx_copy_page(area, GFX_COPY_DIRECT|GFX_COPY_VFLIP): копия области из
неактивной страницы в активную; банк 0x50, то есть приёмник обновляется и
в VRAM, и в ОЗУ-копии — иначе heal восстанавливал бы старый фон;
- tests/pageflip: 5/5 PASS в MAME (прямая, vflip, рамка, широкая 320 двумя
проходами, источник цел).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кэш — ленивый постраничный, живёт до конца уровня; ENTER-ROOM-FAST делается
шагом 4 на готовом gfx_copy_rect. Открытых вопросов в плане не осталось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разведка железа первым шагом (смена Port_Y между burst-триггерами и
адресация двух страниц в W3), ядро копии с реверсом Y, vflip-блиты для
row-major, gfx_copy_rect экран-экран; в приложении — состояние, единая точка
пересчёта Y, фон через pop_tile.c, зеркальные кадры персонажей, клип.
Побочной задачей ENTER-ROOM-FAST на том же кирпиче.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO.
- pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225,
ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда).
fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33.
proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра.
- pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес
безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и
удар в ветки stepfloat/bumpfloat (плавные кадры, без урона).
- roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ);
сброс pop_feather в pop_start_level (seg003:189).
- Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе:
3/4 зелёный, 5/6 СИНИЙ, остальные красный. Mono-блиттера с параметром
цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py
кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие),
pop_potion_draw выбирает набор. Атлас 23 -> 37 спрайтов (+423 Б).
- Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не
ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился
дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP.
Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)),
а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным.
Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов
разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host
5/5 (1727 в [phys]), size-check OK.
Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида;
синее зелье без своей вспышки).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три бага одной отладочной сессии, все проверены в MAME пользователем.
1. SHADOW-FIGHT-L5. Кид доставал меч, а тень вставала в боевую стойку и
рисовалась спрайтами стража. В pop_check_can_guard_see_kid первым
множителем стояло `Guard.charid != 0`, тогда как оригинал (seg003:702)
пишет `Guard.charid != charid_1_shadow || current_level == 12`: тень
боевая только на 12-м уровне. Оба симптома шли из одной ветки
control_standing (seg005:352). Возвращён и пропущенный множитель
`Guard.direction != dir_56_none`.
2. SEAM-BUTTON-STALE. Кнопка в шве не меняла вид ни при нажатии, ни при
отжатии. Сигнатура редроя шва сравнивала room_modif, а у кнопки modif —
это индекс LINKLOC, константа уровня: нажатие живёт в doorlinks2, и в
сигнатуру не приходило никогда. seam_row_sig подмешивает бит нажатости.
Оригинал шов в этом случае не перерисовывает вовсе — расхождение
осознанное, записано как D-2 в docs/impl_diff.md.
3. BUG-POTION-STRIPE. Синяя лента паласа пропадала на месте выпитого зелья
и не возвращалась даже после перезахода в комнату. У оригинала
curr_room_tiles — сама таблица уровня, у нас fg живёт в двух местах, и
страницу правил главный цикл уже после pop_process_trobs. В этот зазор
trob зелья крутил фазу пузырька поверх обнулённого модификатора:
bubble_next_frame(0) = 1, а для пола в паласе modif 1 означает «узор не
рисовать» (seg008:499). do_pickup теперь правит страницу уровня сразу.
Тем же лечится меч: animate_sword делал `--mod[tp]` из нуля.
tests-host 5/5 (добавлена заглушка pop_level_set_tile), size-check чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Доска отставала от кода на три задачи — планировать по ней было нельзя.
Сверка проведена грепом по исходникам, а не по записям:
- L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME
(зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с
рождением тени, левый клип тени). Протокол с разбором решений — в архиве;
- L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммиты dc0bd47, 4d4323f,
db4106a, 1461ed5), на доске значились как предстоящие;
- тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type),
pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в
коллизии (tile_is_floor совпадает с seg006:0628);
- в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан,
скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW.
Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные
обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как
отдельные задачи отменены, вместо них политика приёмок в архиве.
Новая цель — уровень 5. Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё
портировано на уровнях 1-4. Единственная новая механика — спецсобытие «тень
крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP
(check_shadow / do_init_shad / do_auto_moves + shad_drink_move /
autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая
готовые константы и то, что у нас уже есть под это.
Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но
не в снимок room_fg, по которому работает коллизия — если зеркало поставлено,
пока игрок В комнате 4, оно невидимо для коллизии (тень не родится). В
обычном прохождении недостижимо: дверь выхода в другой комнате. Записан
точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк.
Правило «в _OPEN только незакрытое» теперь выполняется буквально:
- bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки
обновлены во всех документах и в комментарии pop_trob.c);
- из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS,
L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив;
- из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в
BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты);
сводная таблица пересобрана по реально открытым записям.
Все внутренние ссылки проверены скриптом: битых якорей 0. make size-check
OK (65 программ), tests-host 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NOT. У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер. Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись. Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst). В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.
Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит. putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково. tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.
tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла. 0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).
applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта. Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tile_at_kid (guards.c) считала колонку честным / и %, хотя резидентная
POP_TILE_DIV — это и есть tile_div_tbl оригинала, и остальной порт давно на
неё переведён. У SDCC z80 пара / и % над int это __divsint плюс __modsint,
который внутри снова зовёт __divsint — ~5 400 тактов на вызов.
Зовут её в ЦИКЛЕ по колонкам между стражем и Кидом
(check_can_guard_see_kid, seg003:761). Когда Кид у шва, его curr_col = −1,
луч тянется через всю комнату: замер дал ВОСЕМЬ пар делений за кадр,
около 43 000 тактов = 10 % растрового кадра, в фазе логики. После фикса
таких вызовов не остаётся.
Как ловилось: брейкпоинт на __divsint с печатью адреса возврата дал
ret=C033 восемь раз за кадр; остановка на нём с dasm при замапленном банке
показала HL−65 / ld de,#14 / call __divsint по смещению 0x24 банка 1.
Снята и ложная тревога из прошлого коммита: пролог pop_char_fore на шве НЕ
разбухает до 134 730 — это была ошибка зонда (адрес fore_tile в банке
совпадает с кодом других банков, в интервал попадали чужие срабатывания).
Чистый замер: пролог 16 950, как и в середине комнаты. Урок записан в
«Как мерить» в docs/perf_backlog.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три шага пункта 0 из docs/perf_backlog.md.
1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком
лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает
— возврат до первой заливки. Замер: от входа в узор до конца всего
прохода 6 055 тактов вместо ~60 000 на тайл.
2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту. pop_blit_b тоже
отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски
кладки высотой 3..12 px он пропускал и платил полный
atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить,
что рисовать нечего. Размеры сняты из каталогов атласов и заданы верхней
границей по группе.
3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая
марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих
разделителях и обеих марках.
Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip
выключен, шесть тайлов в fore-окне, три из них — дворцовая стена):
тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо
171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4.
Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на
месте.
Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а).
Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана.
BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка
ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки
(Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac).
Меч в инвентаре не теряется. Это болезнь чита: в оригинале телепорта между
комнатами нет и в режим боя без соперника попасть нечем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ОПТИМИЗАЦИЯ. Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом. Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit. Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights). Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.
BUG-LOOSE-BUTTON-1. Упавшая плита не нажимала кнопку. Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе. Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается. Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).
BUG-GATE-FF-1. 0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate. gate_passable заворачивал Кида в
воротах, нарисованных открытыми. Мёртвая ветка убрана.
BUG-TORCH-CHOMP-1. Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа. Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.
Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.
Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбивка логического кадра брейкпоинтами (уровень 4, Кид неподвижно на
(1,7)): работа 517 609 тактов = 120% растрового кадра, период 4 кадра.
Циан (PROF(6)) — 233 515 = 54% растрового кадра, из них
pop_char_fore(KID) = 171 693.
Внутри fore-прохода шесть тайлов, и два из них — СТЕНА ряда 2 под ногами
Кида — стоят 74 310 и 65 553. Дворцовая кладка на тайл: 6 wpp_fill
(~3 100) + 5 pop_wall_b (~9 760) ≈ 60 000. Окно клипа в этот момент
x 229..241, y 106..147 (прочитано из pop_t_fclip_*), верхний кусок узора
стоит на y=157 — не пересекается вовсе, тайл (2,6) промахивается и по x.
То есть 139 863 такта за кадр рисуют ноль пикселей; снятие уводит период
с 4 растровых кадров на 3.
Записано пунктом 0 в docs/perf_backlog.md с тремя вариантами лечения.
Состав узора сверен с SDLPoP (seg008.c:1943) — порт дословный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Клипованный путь вынесен в отдельную функцию blit_b_clip: под его девять
16-битных локалей SDCC заводит кадр IX, и за этот кадр платили ВСЕ блиты
фона, включая те, где клипа нет вовсе (весь фон вне fore-прохода — факелы,
зелья, перерисовка тайлов, у них pop_t_fclip_on == 0). Быстрый путь идёт
сразу в gfx_blit_noclip.
Замер: pop_torch_draw 41 778 -> 31 218 тактов на факел (часть разницы —
прошлая правка pop_cd_touch; чистый вклад этой ~6 300 на блит). Поведение
не изменилось: клипованная ветка перенесена дословно.
docs/perf_backlog.md — отложенные идеи с измеренной ценой (футпринт из
физики 11 574, размеры ленты из каталога атласа, один map/unmap на группу
блитов, единый проход по тайлам как redraw_needed_tiles, objtable,
отложенные таблицы back/mid/fore) плюс раздел «как мерить»: wait-state'ы
дают 2,4x к справочным тактам, кадр 430 000, адреса символов меняются
после каждой пересборки, сцена между сессиями не воспроизводится.
Там же — что уже проверено и НЕ сработало.
tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замерено брейкпоинтами в MAME (уровень 1 комната 1, Кид стоит у факела).
Калибровка, без которой цифры не сходятся: такт totalcycles != номинальный
T-такт Z80, wait-state'ы ОЗУ Sprinter дают ~2,4x (get_tile 574 против 1422).
1. Окно перебора коллизии — как у оригинала (left_checked_col..right_checked_col,
seg004:0047), было: все 14 колонок каждый кадр. Признак годности слота у
нас дешевле оригинального: не массив номеров комнат с очисткой, а границы
окна, которые move_coll_to_prev переносит в prev вместе с флагами; бамп
считается по пересечению двух окон. check_chomped_flags тоже ограничен
окном, иначе протухшие слоты дают фантомный перемол.
2. get_tile_div_mod — таблицами tile_div_tbl/tile_mod_tbl (seg006:702), было
/14 и %14. SDCC разворачивал это в __divsint + __modsint, а __modsint
внутри зовёт __divsint ещё раз: 5 400 тактов на вызов, 13 вызовов за
кадр = 16 % кадрового периода на «в какой колонке точка».
3. get_row_collision_data: ряд разрешается один раз на весь перебор (было —
get_tile на каждую колонку, 1 422 такта), грань идёт шагом TILE_SIZEX как
в оригинале, wall_type таблицей вместо switch.
Итог: check_collisions 60 888 -> 42 750, физика Кида 100 578 -> 67 518,
синяя полоса ~60 % -> ~30 % кадрового периода. Профиль остатка и следующие
цели (отрисовка Кида 47 %, process_trobs 21 %) — в TASKS_OPEN.md#draw-cost.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ
разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8
остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден,
bumped() срабатывает. У нас индекс — колонка отрисованной комнаты, тот же
тайл менял слот, и enter_room вынужден был выбрасывать историю целиком —
на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота.
Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не
выбрасывается, а перенумеровывается на 10 слотов. check_leave двигает x
ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с
габаритом Кида — сами флаги инвариантны, меняется только номер слота.
Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет
move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал».
Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация.
Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет
за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS.
Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило
«фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда.
tests-host: все 5 наборов прошли. Приёмка в MAME впереди.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pop_bg.c разрезан по ЧАСТОТЕ вызова, а не по размеру:
pop_bg.c (банк 2) горячее fore-проход, оверлеи, кладка, клип
pop_room.c (банк 7) холодное draw_tile, точечные перерисовки, mob,
загрузка атласов
Стык — три тонкие __banked-обёртки (wall_pattern, wall_pattern_reset,
draw_gate_back): тела остаются непомеченными, поэтому горячий fore-проход,
зовущий wall_pattern до девяти раз за кадр, платит ноль, а трамплин
достаётся только холодному пути — ~40 вызовов на вход в комнату, 26 000
тактов = 0.06 кадра РАЗОВО. Общее состояние (pop_loose_modif, pop_ceil_modif,
obj_row/obj_col) писучее, лежит в _DATA/W2 и видно обеим половинам.
Заодно удалена мёртвая potion_bubble (169 Б).
Грабля: n_banks объявляет само приложение (roomtest.c), а не sprinter-cc.
Восьмой банк без правки константы линкуется молча, _bank_pages[7] остаётся
0xFF, и программа встаёт намертво до первого кадра. Диагноз снят дампом
_bank_pages из MAME.
Замер (ALLOCS=3000): BANK2 11 942 -> 5 872, BANK7 6 035; _CODE и куча не
тронуты (22 556 / 4 301).
Проверено: tests-host зелёные; построчная сверка pop_bg.c+pop_room.c против
дорефакторного pop_bg.c — ни одной строки логики не пропало; 8 комнат в MAME
до/после совпали попиксельно по активному экрану (различия только в фазе
анимации факелов и кадре Кида); живой прогон с переходами комнат, боем и
воротами сверен контрольным запуском HEAD-бинаря.
DRAW-COST поднят в приоритете: замер пользователя (ур.1 комната 3, страж
убит, Кид стоит) — синяя 80%, зелёная 20%, циан 110%, итого ~210 %
кадрового периода В ПОКОЕ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRAW-CHAR. Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS. В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.
pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
любого банка без трамплина. Проход окклюзии тоже один
(pop_fore_over_char), pop_fore_over_kid больше нет.
Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.
Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.
MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта). W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.
MEM-BANK2, шаг 2: дедуп внутри банка. wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.
Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.
Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б. tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет
получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.
Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS,
а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.
Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
обратно. +48 байт W2.
Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
seg008:1B06) и расширение перебора объединённым прямоугольником
«персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
(общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
«оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
вернувшейся кучей костей.
Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.
Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).
Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).
В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт levels_plan.md §2 (шаг 1) — машинерия смены уровня целиком.
- Номер уровня стал состоянием: pop_current_level (порт current_level),
pop_next_level больше не флаг, а НОМЕР; главный цикл срабатывает по
расхождению next != current (порт play_level_2, seg003:0386).
- pop_level_load_num(n): LEVELS\res20NN.bin (fallback a:\), старая
EMM-страница отпускается только после успешной загрузки новой.
На диск кладутся все 15 уровней (34 КБ).
- Потабличные различия (data.h:840..848) — таблицы по 16 в pop_level.c:
tbl_entry_pose, tbl_guard_hp (HP стража ушло из хардкода 3),
tbl_guard_type (−1 = стражей на уровне нет), tbl_level_type.
- find_start_level_door (seg003:02E6): на уровне 2 стартовый тайл —
правая половина двери уровня, без modif=43 + add_trob(...,3) Кид
материализуется внутри глухой створки. Тип 3 = дверь захлопывается
за спиной, как в оригинале.
- HP через уровень: hitp_beg_lev (seg003) — рестарт уровня откатывает
HP к нему, пройденный уровень подтягивает его к hitp_max. Заодно
большая склянка add_life (тип зелья 2: +1 к потолку HP до 10) —
на уровне 2 она есть, комната 20.
- have_sword = level >= 2 (play_level, seg003:106).
- Чит Shift+L — следующий уровень (seg000:698).
Проверено в MAME: уровень 1 → Shift+L → уровень 2 (комната 5, новые для
нас тайлы 8/9 «большая колонна» на месте, дверь захлопнута, HP 3) →
влево в комнату 4 со стражем → Shift+L → уровень 3. Вход в стартовую
дверь уровня 2 по Up не срабатывает — как и должно (start_room-гард).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME,
без образа диска. ucsim_z80 идёт в комплекте нашего SDCC — новых
зависимостей нет.
ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC. У SDCC z80 int 16 бит, у хоста 32, и
расходится это НЕ в объявлениях, а в выражениях: integer promotion
повышает операнды до int независимо от того, объявлены они как uint8_t
или uint16_t. Перевод кода на фиксированные типы разницу не убирает —
убирает только исполнение с z80-семантикой. Побочно проверяется
кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не
видит в принципе.
Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в
структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает
отчёт), host-tests.mk (общие правила). Вывода через printf нет: тестовый
бинарь линкуется без Sprinter-libc. Через ucsim-simif не идём — номера
его команд плавают между версиями, halt + dump работают везде.
Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая:
testkit/t_selftest.c — самопроверка (sizeof(int)==2)
applications/PoP/roomtest/tests-host/ — движок PoP
Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из
pop_geom.c с наивной 32-битной формулой на 128 шагах. Заявка «бит-в-бит
как в SDLPoP» до сих пор держалась на комментарии. Тест проверен
мутацией: порча эталонной константы даёт красный.
Планы дальнейшего покрытия:
docs/host-tests-plan.md — libc и libbgi (не начато)
applications/PoP/docs/host_tests_plan.md — движок PoP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kid_heal/pop_guard_heal звали gfx_heal всегда, хотя рисуют ровно тот
прямоугольник, который блит в большинстве кадров кладёт noclip-ядром.
Все три места heal (+ heal_off фона) сведены к общему pop_heal_fast.
Замер в MAME, счётчики totalcycles на входах kid_heal и kid_tick (вся
группа heal за кадр), комната 1, Кид стоит:
клипающее ядро 26 200 тактов/кадр (149 кадров)
noclip 15 848 тактов/кадр (239 кадров)
−10 352 такта, −39.5 %. A/B в одном прогоне: вторая половина снята с
пропатченным в памяти условием (jr nz → jr), то есть на той же геометрии.
Размер СУММАРНО −362 Б: _CODE +17, BANK2 −116 (свободно 2708 — это тесный
банк из рисков levels_plan §5), BANK3 −263, BANK4 без изменений.
Грабли по дороге: первым заходом хелпер был static inline в pop_bg.h —
SDCC 4.5 И встраивает тело (181 Б) в каждый вызов, И оставляет копию в
каждом TU, который видит заголовок. pop_guard_heal раздулся с ~60 до
663 Б, итого +1091 Б в _CODE и +636 Б в банке стража. Отсюда pop_draw.c:
обычная функция в резиденте W1, из банков это прямой call без трамплина.
pop_room_clip_borders оставлен клипающим осознанно (320 не лезет в 8 бит,
гейт border_dirty редкий) — причина записана в коде.
Проверено визуально: ходьба, прыжок, спуск, позиция за решёткой шва
(straddle — там работает клипающий фолбэк) — артефактов нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три Critical'а (BUG-1 провал на row 1 при боковом переходе, BUG-2 ping-pong
при возврате, BUG-3 окклюзия climb-up на кнопке) висели непроверенными с
2026-07-21. Прогнал в MAME:
- BUG-1 не воспроизводится: room6 → кнопка (0,2) → открытая решётка →
переход влево даёт room8, y=55, curr_row=0. Заодно снят и сам диагноз
записи — репроекция Y при БОКОВОМ переходе не нужна: goto_other_room
(seg002.c:390) меняет только x, наш check_leave делает то же.
- BUG-2 не воспроизводится: шов room2↔room3, четыре пересечения с
разворотом сразу после входа — комната меняется ровно раз на пересечение.
- BUG-3 закрыт фиксом tile_code_drawn от 2026-07-28 (это дубль уже
записанного «спуск с кнопки»); оговорка про непереснятый подъём — в
bug_closed.md.
bug_list.md теперь только открытое (BUG-CEIL-1/2/3, BUG-OCCL-1, T-1, T-2,
таблица обхода 24 комнат) + индекс с якорями. bug_closed.md — закрытое
вместе с разбором корней (odd-pixel char_x, подстановка тайла кнопки, баг
кодогенератора SDCC), он и есть главная ценность архива.
TASKS.md: кросслинки на открытые баги в шапке, в L1-TRIAGE, L1-PASS и
«Отложено». Указатели в CLAUDE.md/README/room_model_plan/layout_plan_v2
переведены на нужный из двух файлов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Документы отстали от кода: PORT_PLAN писал «PoC не начат», хотя играется
весь уровень 1, а четыре плана были исполнены целиком.
- PORT_PLAN: таблица статусов по разделам; фазы 0-3 сделаны, 4-6 нет;
риски §8 п.1/п.3 закрыты, п.2 переформулирован под реальный движок
(спрайтовый движок для персонажей не используется, лимит «21 спрайт»
неприменим), п.4 — найдено расхождение таймингов: оригинал считает
логический кадр за 5 тиков при BASE_FPS=60 (83.3 мс, в бою 100 мс), а мы
ждём три vsync (60 мс) — игра идёт примерно на 39 % быстрее эталона.
- levels_plan.md — новый: машинерия перехода между уровнями, второй
тайлсет (palace), потабличные различия и читы SDLPoP, которые окупаются
сразу. Инвентарь тайлов снят прямо с res200N.bin: уровень 2 не требует
ни одного нового ассета и ни одной новой механики.
- roomtest/TASKS.md — новый: доска текущих задач с критериями готовности.
- Удалены как исполненные и перекрытые кодом: clip_char_plan,
double_buffer_plan, loose_floors_plan, size_optimization_plan. Его §8
(замеры скорости отрисовки) не был перекрыт — перенесён в
layout_plan_v2 §9, чтобы не потерять цифры.
- KID_PLAN / gates_spikes_plan — шапки «реализовано, оставлено
справочником»; room_model_plan — «S1 сделан, остальное не срочно».
- docs/README.md стал индексом с отметками актуальности.
- ideas_backlog: зелье переворота экрана — оригинал переворачивает готовый
буфер построчно, спрайты не трогает; по данным уровней тип 4 встречается
только на уровне 9, до него механика не нужна.
- examples/scroll: ссылка на удалённый план вела к неверному факту
«теневая копия одна — общая»; заменено на подтверждённое «у каждой
страницы своя».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём
8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает
prandom(1), вышла бы шахматка), и главное — сколько это реально даст.
Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не
основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова.
Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же
asm-процедуру (один call вместо трёх), и только потом менять генератор.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мышь игре не нужна (управление — raw-клавиатура, которую мы и так
забираем у DSS), а её прерывания воруют такты из бюджета, занятого на
86 %. Эффект измерен побочно: при движении мыши на хосте кадры выбивались
до 1.5 кадрового периода, при неподвижной — 225 кадров без превышений.
Записано с тем, что проверить (есть ли в RST 30h выключение, сколько
стоит одно прерывание, восстановление состояния на выходе) и почему не
сейчас: выигрыш только когда игрок двигает мышью, риск оставить систему
без мыши после выхода — заметный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разгрузка W1/W2 перед ИИ стражей (вариант 2 из двух обсуждённых).
1. pop_guard.c разделён по окнам: состояние/логика (Guard, HP, enter,
kill, load_frame) остаются в W1/W2 — их обязан видеть банк guards.c;
ОТРИСОВКА уехала в новый pop_gdraw.c, собираемый как --w3 (резидент).
Правило границы: резидент = только то, что рисует и зовётся
исключительно из главного цикла. Кадр стража стал глобальным
(pop_gframe): заполняет логика, читает резидент.
2. Страж не окклюдировался передними гранями тайлов — рисовался поверх
столба. В оригинале любой Char это запись midtable, а foretable
рисуется после всех midtable (draw_tile_fore, seg008:690), т.е. столб
перекрывает всех одинаково. Футпринт персонажа выделен из
pop_fore_over_kid в char_footprint(), поверх него добавлен
pop_fore_over_char() — слой fore + полоса потолка, без оверлеев поз
виса/полёта/подъёма (у стража их нет; появятся — портируем
redraw_at_char2 общим кодом, а не догадками).
3. Упаковщик стража: тот же off-by-one, что уже ловили у Kid.
load_chtab_from_file(id_chtab_5_guard, 750) даёт images[0] = res751,
а рисование индексирует images[frame.image] — значит image=N это
res(751+N), а не res(750+N). Из-за сдвига frame_166_stand_inactive
рисовался как res767 (выпад) вместо res768 (стойка).
Проверено в MAME: страж в комнатах 3 и 21 стоит в правильной позе;
окклюзия подтверждена патчем Guard.x в живой сессии — при заходе за
столб спрайт корректно срезается его передней гранью.
Память: _CODE 26 149 -> 25 703, куча W2 805 -> 1245 Б, резидент W3
11 656 -> 12 819 (свободно 3565 Б), банк 1 236/16384 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новый документ applications/PoP/docs/layout_plan_v2.md по свежему замеру
(коммит 1214785): точные размеры окон/модулей/функций/данных, уточнённая
модель банкинга и пошаговый план.
Главное уточнение против v1: из __banked-кода резидент W3 недостижим — и
транзитивно тоже (bank -> pop_map -> pop_bg сломается). Отсюда целевая
раскладка: W3-резидент = графика, которую зовёт только главный цикл;
W1/W2 = ядро, достижимое отовсюду (включая банки); банки = новая холодная
логика (стражи/боёвка). Проверено по libbgi: скобка _bgi_begin/_bgi_end
сохраняет и возвращает ТЕКУЩУЮ страницу W3, поэтому примитивы libbgi
можно звать и из банка; нельзя лишь открывать скобку из кода, лежащего
в W3.
Крупнейшие цели: kid_data.h (3745 Б таблиц в _CODE) -> EMM-страница с
портом load_frame/cur_frame; вынос loose/потолка из pop_map в W3 (делает
pop_map bank-safe); дедуп геометрии в pop_geom.c; разгрузка _DATA.
Попутная находка: --w3 принимает ОДИН файл на флаг, поэтому в Makefile
"--w3 pop_trob.c pop_map.c" кладёт в W3 только pop_trob, а pop_map едет
в W1/W2 (в build-каталоге остался устаревший w3_pop_map.rel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спуск Кида с кнопки (room8, кромка (0,6)) рисовался неверно: Кид просвечивал
в щель между кнопкой и ближним столбом, а ближняя рука была срезана до одного
оторванного пикселя. Две независимые причины.
1. Не был портирован clip_char() (seg006:1749) — оригинал перед add_objtable
обрезает спрайт персонажа сверху по y_clip[curr_row+1], когда тайл над
головой стена или пол. Порт: pop_clip_char_top() (pop_map.c, метрики по
set_char_collision) + новый примитив gfx_blit_cols_part() в libbgi (блит
column-major с пропуском skip верхних строк; обрезка сверху бесплатна —
колонка непрерывна в ОЗУ, сдвигается только старт). gfx_blit_cols стал
тонкой обёрткой над ним. kid_heal чистит уже ОБРЕЗАННЫЙ прямоугольник,
иначе стирается кромка пола над срезом.
2. climb_overlay_tile (порт draw_floor_overlay, seg008:1E3A) выбирал ветку по
СЫРОМУ коду тайла, а get_tile_to_draw (seg008:240) подменяет нажатую кнопку
на floor/stuck. Тайл-кнопка не проходил тест floor → уходил в
draw_other_overlay, который кладёт поверх Кида ВЕСЬ тайл вместо узкой
кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn(): одна
подстановка на все слои (fore_only_tile/overlay_mid_tile перестали её
дублировать, W3 −293 Б).
Проверено покадрово в MAME (шаг gv + снимок): кадры спуска 148..138 чистые,
после приземления на кнопке мусора нет.
Заодно: gfx_heal_noclip() (пара к gfx_blit_noclip; heal 11658 -> 8982 тактов)
и rest_pending в pop_trob — холостой проход по 30 тайлам только когда есть
отложенные редрои пик/кнопок (pop_process_trobs 89-111К -> 70-92К тактов).
START_ROOM временно = 8 (отладка спуска с кнопки), вернуть на 6/старт уровня.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>