Клипованный путь вынесен в отдельную функцию 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>
Сближение с SDLPoP, замером НЕ подтвердилось — фиксирую как есть.
- FORE_ANY[32]: есть ли у кода тайла хоть что-то в переднем слое. Порт
ранней проверки оригинала `if (tile_table[curr_tile].fore_id == 0) return;`
из начала ветки default в draw_tile_fore (seg008:0D15), расширенной нашими
спецслучаями (стена 20, пики 2, чомпер 0x12, нижняя грань loose 11 — они
рисуются мимо fore_id). Была в самом конце цепочки сравнений.
- ft_code/ft_x/ft_dmy: контекст тайла считается один раз на тайл, как глобалы
curr_tile/draw_xh/draw_main_y у load_curr_and_left_tile. Код тайла читался
дважды (fore_tile и снова fore_only_tile), координаты — в каждом листе.
Замер (Кид на 2,4 в щебне, MAME): pop_fore_over_char 58 764 -> 58 866, то
есть в пределах шума. Ранний выход не срабатывает — в футпринте Кида тайлы
почти всегда С передним слоем; снятое второе чтение кода съедено проверкой
FORE_ANY и записью контекста. Отрисовка не изменилась: щебёнка блитится теми
же параметрами (x=150 y=208 sx=22 w=10 h=2).
Где время на самом деле: ~16 000 тактов фиксированных накладных на КАЖДЫЙ
блит фона независимо от размера (atlas_image + gfx_w0_map + чтение w/h через
окно 0 + арифметика клипа + unmap). Блит щебёнки 10×2 обходится в 21 168,
факел 16×22 — в 31 000 при самом gfx_blit_noclip 8 706.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Главный цикл спейсится тремя gfx_wait_vsync, поэтому работа сверх 430 000
тактов стоит сразу целый лишний растровый кадр. Было 470 964, стало
~425 600 — игра быстрее на треть (16,7 логических кадров/с против 12,5).
- pop_y_to_row: цепочка сравнений вместо (y+60)/63%4-1. ВАЖНО: медленный
хвост вынесен в ОТДЕЛЬНУЮ функцию — SDCC видит одинаковое выражение в двух
ветках и поднимает деление в вершину, быстрые возвраты не спасают.
- col_from_x (pop_bg) и get_tile_div_mod (pop_map) — общие резидентные
таблицы POP_TILE_DIV/POP_TILE_MOD в pop_tile.c (const банка из чужого
банка не читается).
- pop_fore_over_char: расширение окна считается арифметикой, а не перебором
10 колонок и 3 рядов (условие монотонно -> границы). Формулы сверены с
прежним перебором перебором значений, расхождений нет.
- pop_cd_touch: цикл по страницам развёрнут, x+w/y+h считаются один раз.
Зовётся с каждого блита фона, стоил 6 846 тактов.
- process_trobs: tp/10 и tp%10 у факелов — таблицей.
- Пустой слот соперника (стража на сцене нет, на странице ничего не
нарисовано) считается «тихим»: ни heal, ни вход в pop_char_draw, ни
fore-проход.
Приём для поиска делений: брейкпоинт на __divsint/__divuint/__divuchar с
печатью адреса возврата (printf "%04X", w@(sp)).
Профиль остатка — TASKS_OPEN.md#draw-cost. tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- move_coll_to_prev -> memcpy (LDIR): цикл на C пересчитывал адрес
назначения через слот кадра IX и обходился в 5 514 тактов на 14 байт.
- Окно перебора режется на НЕПРЕРЫВНЫЕ пробеги (комната слева / своя /
справа), по каждому идёт coll_scan с шагающим указателем. Прежний
«быстрый путь для окна внутри комнаты» не срабатывал почти никогда: Кид
в колонке 0 даёт окно с −1, и всегда шёл медленный сбор во временный
буфер с тернарником на колонку (1 340 тактов на колонку).
- Пролог ряда (координата грани, длина окна, смещение слота) вынесен из
тела ряда на кадр; базы соседних рядов — те же ±10 без пересчёта.
check_collisions 44 022 -> 38 334, физика Кида 67 518 -> 63 102, работа за
логический кадр 470 964 -> 466 560 (бюджет растрового кадра 430 000).
Замер итерации: пустая колонка 750 тактов, колонка-стена ~1 700.
tests-host: все 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>
Сценарий: ур.1 комн.8, ряд 0, x=170, лицом вправо, решётка шва закрыта —
разбежаться вправо и не отпускать. Кид проходит сквозь решётку.
Корень (две трассы, наша и оригинала, стартовая позиция совпала до пикселя):
смена комнаты в этой точке ШТАТНАЯ и у нас, и в оригинале — leave_room вправо
блокируют только doortop. Оригинал держит Кида бампом СРАЗУ ПОСЛЕ перехода,
потому что check_collisions хранит флаги по паре (колонка в СВОЕЙ комнате,
номер комнаты) и сравнивает prev/curr только при совпадении номеров: решётка
комнаты 8 и до, и после перехода лежит в слоте 9 с room=8, история не рвётся.
У нас индекс — колонка относительно отрисованной комнаты, при переходе все
индексы уезжают на 10, поэтому enter_room зовёт pop_coll_invalidate, тот
подавляет бамп на кадре входа, а дальше перехода флага 0->1 уже не будет.
Что делать — порт индексации оригинала (10 слотов по колонке своей комнаты +
массив её номера); тогда pop_coll_invalidate не нужен вовсе. Осторожно: это
сердце коллизии из BUG-SEAM-PINGPONG.
SDLPoP инструментирован для сверки: POP_TRACE=1 включает покадровую печать
Char + обе пары краёв, плюс маркеры BUMPED и LEAVE. В bug_list записана и
команда сборки на macOS (штатный Makefile требует pkg-config, которого нет).
073a6e0 вернул в safe_step(distance == 0) оригинальный seq_39 — а это шаг на
ОДИННАДЦАТЬ пикселей, который останавливает только бамп. У ворот шва
(ур. 1, комн. 6) наш бамп срабатывает не на том же кадре, что у оригинала
(своя история — BUG-SEAM-PINGPONG), и Кид с разбега ПРОБЕГАЛ сквозь
закрытую решётку между комнатами 8 и 6. Мелким шагом упирался нормально —
то есть промах именно в длине шага, а не в самом бампе.
Теперь ветка разделена по препятствию:
ЧОМПЕР — оригинальный seq_39: бампа там нет по определению (is_obstacle
требует modif == 2), и шаг обязан пройти целиком, иначе Кид топчется на
1 px и не успевает между челюстями;
СТЕНА И ВОРОТА — прежний «шаг-1»: осознанное приближение, проверенное
трассой живого SDLPoP в BUG-SEAM-PINGPONG. Останется таким, пока наш
бамп не сойдётся с оригиналом покадрово.
Симптом (уровень 3, комната 22): после спуска с уступа Кид проваливался
СКВОЗЬ пол; при подъёме проскакивало лишнее движение вперёд с прыжком вверх.
Корень. play_seq держит W0 замапленным на страницу данных Кида весь цикл и
читает seqtbl прямо через окно (макрос SEQ). Вызов pop_start_chompers,
поставленный в dc0bd47 внутрь обработки SEQ_UP/SEQ_DOWN, лезет за тайлами
уровня: pop_level_access_begin/end — это ровно gfx_w0_map/unmap. После
возврата цикл продолжал читать байткод из закрытого окна, то есть исполнял
мусор.
Фикс: SEQ_UP/SEQ_DOWN только взводят флаг, а pop_start_chompers зовётся
после выхода из цикла, когда W0 уже размаплен. Эффект тот же — трасса
кадра не меняется, чомперы заводятся в том же кадре.
Заодно убрана вложенность того же рода внутри самого pop_start_chompers:
start_anim_chomper получает mod параметром, а не зовёт pop_trob_modif —
тот при первом обращении к комнате сам перемапливает W0 под её bg.
Остальные точки вызова (start_fall, land, enter_room) проверены: там окно
W0 не открыто.
safe_step при distance == 0: возвращена ветка оригинала (seg005:0604) —
seq_39 (шаг 11) вместо нашего «шага-1». Для СТЕНЫ результат прежний: первый
же dx(1) даёт бамп, ровно как в трассе живого SDLPoP из BUG-SEAM-PINGPONG.
Для ЧОМПЕРА бампа нет (в разомкнутой фазе он не препятствие), и оригинал
уносит Кида на все 11 px — а мы шагали на один. Это и был «микрошаг вместо
нормального короткого шага» перед челюстями.
ВНИМАНИЕ на приёмке ур.1: это тот самый safe_step из BUG-SEAM-PINGPONG —
проверить комнату 6, осторожный шаг вплотную к воротам шва.
BUG-CHOMP-JUMP-1 (низкий, маловоспроизводим, на пререлиз): прыжок с места
вплотную к чомперу иногда даёт кадр с отступом назад. В запись сложено всё,
что выяснено: seq_3_standing_jump состоит ТОЛЬКО из положительных dx (значит
отступ даёт bumped, а не анимация); is_obstacle для чомпера у нас совпадает
с оригиналом; прямая трасса (UP+RIGHT одновременно) отката не показала —
главная гипотеза в порядке нажатий (↑ раньше → уводит в up_pressed с
выравниванием x). Там же метод ловли.
T-2 (idle-skip) закрыт — сделан шире, чем формулировался, как DRAW-COST
шаг 1 (a25ce58).
forward_pressed (seg005:0577) исключает чомпер из правила «у стены шагаем,
а не бежим»: `edge_type == EDGE_TYPE_WALL && curr_tile2 != tiles_18_chomper
&& distance < 8`. У нас исключения не было, а wall_type(18) = 3 — чомпер
считается стеной, — поэтому из позиции вплотную к челюстям Кид сначала
делал safe_step на 1-2 px и только потом бежал. Лишние кадры шага не дают
проскочить между челюстями (поймано на приёмке, комната 3.22).
Добавлен pop_edge_tile() — порт curr_tile2 после get_edge_distance.
control_turning не трогаем: у нас он ванильный, а второе такое исключение
в SDLPoP сидит под фиксом fix_turn_running_near_wall.
Две ошибки в одном месте. 1) Кадры 106..110 и передняя кровь 119..123
упакованы в pop_fore.atl (FORE_ENV_IDS в pop_pack_bg.py), а блитились через
pop_env_b — id уходил в env-страницу 3 по индексу 10, где записи нет, и
передний слой не рисовался вовсе: Кид, стоящий ЗА челюстями, был виден
целиком. 2) В draw_tile была только backtable-часть; у оригинала фронт
чомпера рисует отдельная ветка draw_tile_fore (через таблицу он не идёт —
у записи 0x12 fore_id нулевой), поэтому передние зубья появлялись лишь там,
где по тайлу прошёлся fore-проход персонажа.
Порт SDLPoP:
animate_chomper / start_chompers / start_anim_chomper /
next_chomper_timing (seg007) -> pop_trob.c;
draw_tile_anim + draw_tile_fore, ветка tiles_18_chomper (seg008) ->
pop_room.c (низ/кровь/верх, backtable) и pop_bg.c (передний слой);
check_chomped_kid / check_chomped_guard / chomped (seg004) -> pop_map.c.
Состояние — в room_modif, как у пик и ворот: младшие 7 бит фаза 1..N,
старший бит «перемололо кого-то» (кровь остаётся на тайле навсегда).
Номер позы chomper_fram1 и передние куски лежат в РЕЗИДЕНТЕ (pop_tile.c):
их читают обе половины слоя фона, а const-таблица банка из чужого банка
не видна.
start_chompers зовётся там же, где в оригинале: SEQ_UP/SEQ_DOWN в play_seq
(pop_kid.c), start_fall и land (pop_map.c), вход в комнату (roomtest.c).
Поэтому чомперы щёлкают только пока персонаж в ИХ ряду — так в оригинале.
check_chomped_guard у оригинала отдельное тело (страж не проходит через
check_collisions). У нас та же формула уже есть в get_row_collision_data,
поэтому флаги ряда считаются во ВРЕМЕННЫЙ массив: coll_curr/above/below —
это кадр Кида, из них move_coll_to_prev берёт прошлые флаги, затирание
сломало бы ему бамп.
Период смыкания — POP_CHOMPER_SPEED в pop_tune.h (15, как в оригинале).
Заодно: устаревшая заглушка pop_fore_set_clip в tests-host была __banked,
хотя функция давно переехала в резидент — всплыло при пересборке.
Ассеты уже были упакованы (pop_pack_bg.py, 2026-08-07). Банк 6: 20.0 %,
банк 7: 39.7 %, банк 3: 53.8 %. tests-host зелёные (5 наборов).
Зубья в MAME рисуются; анимация и смерть — на ручной приёмке.
Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: heal, блит и fore-проход пропускаются целиком.
Не спецкейс «мёртвый страж», а общее правило — покрывает и труп, и
стоящего Кида, и ждущего стража.
Механизм: снимок входов по страницам (pop_cdraw.c, cd_sig/cd_quiet) +
позиционная метка «фон трогали вот здесь» (pop_cd_touch в резидентном
pop_tile.c, зовёт сам pop_blit_b). Решение перепроверяется перед
отрисовкой, а pop_char_draw страхуется собственным heal — если тик всё-таки
сдвинул персонажа, прошлый кадр стирается там. Слоты рядом (32 px) —
перерисовываем оба, иначе heal соседа выест кусок из «тихого».
Метка обязана быть ПОЗИЦИОННОЙ: с флагом «фон трогали хоть где-то» выигрыш
был ровно нулевым — факелы анимируются каждый кадр и гасили пропуск для
всех сразу (597 684 такта, как без оптимизации).
Замеры (MAME, брейкпоинты по totalcycles, бюджет кадра 430 000):
комн. 1.3, труп стража, Кид стоит: 210 % -> 116 % (500 772 такта),
ноль вызовов pop_heal_fast за кадр, весь фон — 2 блита (44 136);
комн. 1.1, Кид стоит вдали от факелов: 404 112 (94 %), цикл 4 -> 3 кадра.
Узкое место сместилось на ЛОГИКУ: 60 % кадра уходит на тик персонажей,
которые СТОЯТ, ещё 28 % — на loose_tick + process_trobs в комнате без
единой ловушки. Разбивка и план — TASKS_OPEN.md#draw-cost.
Комнаты 2 и 3 отличаются ровно телом убитого стража, и оно даёт +20% синей
и +50% циана, то есть ~70% кадрового периода. Причина видна в коде: ни
roomtest.c, ни pop_cdraw.c не смотрят на Char.alive — мёртвый страж каждый
кадр проходит весь путь живого (heal + блит + clip_char + брызги + клинок +
перебор тайлов fore), хотя его кадр постоянен до выхода из комнаты.
Кандидат на фикс — запечь труп в фон, как loose-плиты.
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>
Точки вызова стоят и в обеих ветках check_bumped, поэтому регрессия
проявится не в зацепе, а в обычном ударе о стену с зажатым Shift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L3-CHKP (чекпойнт уровня 3). Флаг взводится, когда Кид уходит ВЛЕВО ИЗ
комнаты 7, а do_startpos по нему подменяет старт на комнату 2, тайл (0,6),
лицом влево и снимает loose-плиту (7,0,4). Тонкость, на которой я сначала
ошибся: level3_set_chkp (seg002:0665) вызван из leave_room ДО
goto_other_room, поэтому `Char.room == 7` — это комната, ИЗ которой уходят,
а не в которую входят. Поймал пользователь прогоном в SDLPoP: смерть В
комнате 7 вернула его в стартовую 9, а плита осталась цела. Проверено в
MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2;
после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше.
Зацеп ПРЯМО В ПРЫЖКЕ (check_grab_run_jump, seg006:1228) — портирован за
выключателем POP_ENABLE_JUMP_GRAB. Это НЕ ваниль: в оригинале зацепиться
можно только в начале падения (кадры 102..105), то есть Shift приходится
жать уже в полёте; у SDLPoP это enable_jump_grab, и работает он лишь при
включённых fixes-and-enhancements. Три точки вызова как у оригинала:
check_action и обе ветки check_bumped (зацеп за верх стены вместо удара).
pop_tune.h — настраиваемые константы в одном месте (аналог
custom_options_type SDLPoP): чекпойнт, выключатель зацепа и отладочная
крутилка POP_DBG_GATE_HOLD (сколько кадров решётка держится поднятой;
оригинал 5, потолок 30 — таймер связи пятибитный, 31 = «заклинено»).
Задача TUNE-1 в TASKS.md: читать это из ini рядом с exe.
СБОРКА. --max-allocs-per-node снижен со 100000 (дефолт sprinter-cc) до
3000 (дефолт SDCC) через `make ALLOCS=...`, а pop_trob.c уехал в БАНК 6 —
на 3000 резидент иначе не влезает (замер: конец _HOME 0xBC69 при стеке с
0xBB00). Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо
2298. Релизная сборка — make ALLOCS=100000; сравнивать занятость банков
можно только при одинаковом ALLOCS.
Отдельно (вне git, SDLPoP в .gitignore): из референса вычищена вся наша
отладка DBG-GRAB — трасса JMP, GRAB try/probe/fail/OK/skip, автоскриншоты
seg003, печати DBG mob/mid/overlay/kidobj и счётчик dbg_shots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оба фикса подтверждены игрой, записи с разбором корней переехали в
bug_closed.md. BUG-GATE-PASS-1 остался ждать сценария, но его оговорка
про BUG-GATEMOD-1 обновлена (тот закрыт). BUG-SPIKE-1 понижен до низкого
приоритета: маловоспроизводим, смертельность пик подтверждена замером.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено в игре: комната 11 уровня 2, страж выталкивает Кида в 22, Кид
возвращается бегом — страж оборачивается и достаёт меч. Разбор корня
(is_guard_notice не взводился ни в одном из пяти мест оригинала) переехал
в bug_closed.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запись переведена в «ждёт сценария»: чистый пробег по убранным пикам
убивает (трасса по кадрам в записи), а наблюдавшееся «нет урона» — это
уже выдвинутые пики, безвредные для бегущего и в оригинале. Открытым
остаётся только визуальное расхождение со скриншотом SDLPoP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Watchpoint на модификатор пики (ур.2 к.6, тайл 13) на чистом пробеге:
modif=1 — кадр 11 (беговой), x=112, col=2
modif=2 — кадр 12 (беговой), x=117, col=3 (Кид на тайле, h=2)
modif=3 — кадр 177 (frame_177_spiked) (напоролся)
То есть check_spike_below/check_spiked/is_spike_harmful и тайминг
выдвижения верны, «раннего» триггера нет. Настоящий корень: пики
ЗАЛИПАЮТ выдвинутыми — пока габарит Кида накрывает колонку, каждый кадр
start_anim_spike переставляет отрицательный модификатор обратно в 0x8F.
Выдвинутые пики (h=1) для бегущего безвредны по правилам оригинала, отсюда
обе жалобы: пробег не убивает и острия остаются на экране.
Открытый вопрос сузился до 2–3 пикселей: код start_anim_spike совпадает с
оригиналом дословно, значит на скриншоте SDLPoP Кид стоит чуть левее и
колонку пики не задевает. План закрытия — в записи.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый
(BUG-SPIKE-1, пики) заведён с замером и гипотезой.
BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену.
check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал
(seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые»
флаги из нужного. Наш prev=3 («уже перекрывал») подавлял бамп ровно на
кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1
на стене приходился как раз на него. Порт трёх рядов дословно.
Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20
стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ
изменились — правка поведение-сохраняющая. Живьём подтвердил пользователь.
BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому
неактивный страж не оборачивался на Кида за спиной никогда. Портированы
все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound,
мягкое/среднее приземление, обрушенная плита, щелчок кнопки. Ждёт
игровой проверки боем в комнате 11 уровня 2.
BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0). Сосед угла снизу-слева
лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной,
оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет.
Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты.
BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком. pop_ceil_bake_empty
стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет
графика соседа слева и верхушки ряда 0. Теперь перерисовываются ряды −1 и
0, колонки col−1..col+1. Проверено попиксельной сверкой с эталонной
перерисовкой: 0 различий.
Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых
нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24
на уровне 1, только комнаты полностью пустые.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три правки едут вместе намеренно: n_banks и --bank обязаны меняться
атомарно, иначе промежуточный коммит — зависание (см. ниже).
MEM-BANK5. CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому
килобайт кода, уехавший в банк, — это килобайт, доступный данным.
Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления
дёргается раз в кадр на персонажа и горячих банк→банк переходов не
создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на
КАЖДЫЙ тайл).
_CODE 26 780 -> 24 662 Б (−2 118)
куча 180 -> 2 298 Б
банк 5 2 211 / 16 384 (13.5 %)
Шина control_* (8 глобалов) переехала в pop_state.c. Сегодня она уцелела
бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные
остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину
трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод
ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held.
Заодно pop_ctrl.c наконец включает собственный заголовок — раньше
объявления жили прямо в нём.
ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в
roomtest.c захардкожен, и его надо править вместе с числом --bank. С
n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5]
остался нулём, и первый же вызов pop_ctrl_init() через трамплин
отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение
забрело в дисковый код DSS и осталось крутить чтение секторов. Симптом:
загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид
успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и
kid_tick не вызывается ни разу. Ровно предупреждение из шапки
runtime/bank.s. Сверку n_banks с реальным максимальным индексом банка
записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки.
DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828),
по фронту нажатия, под pop_cheats. Нужны потому, что мост MAME теряет
нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя —
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть
чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки). Навигация
обязана только телепортировать.
Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP,
номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и
[ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142,
колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 ->
151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5.
Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком:
не хватало трёх веток выбора последовательности и уборки меча в ножны.
Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно
на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз.
Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160.
BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола
перед толчком, а был заглушкой «полировка K3». Суммарный dx seq_4 —
62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с
кромки: без выравнивания Кид не перепрыгивал его никогда. Порт —
pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг
не попал в [-8,-1]»). На харнессе: было — толчок с x=165, кадр 44 в
колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт
x=87 col=1 row=1. Остальные 8 сценариев не изменились.
BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для
зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8)
стартовали закрытыми. Добавлены ветки gate (1 -> 188) и loose. Ветка
wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам
соседей в момент отрисовки. На уровнях 1-3 таких ворот всего двое
(ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0
ведут себя одинаково.
Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap()
оставлен как многоразовый инструмент.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти
всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько
кадров. Харнесс гоняет тот же кадр, что и главный цикл
(pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и
сравнивает трассу состояния с эталоном.
- scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса;
sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла).
- stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт
kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает).
- t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/).
- t_grab.c — окно зацепа: существует, достижимо коротким шагом, не
зависит от рисунка нажатий.
- record_golden.py — снятие эталона по одному сценарию за прогон.
- testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS.
Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем
самым перевёл поиск BUG-GRAB-1 на клавиатуру.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Отличие от остальных записей раздела «НЕ БАГИ»: там картинка кривая и
совпадает с оригиналом лишь потому, что оригинал сам так рисует (порядок
midtable). Здесь же спрайт перекрывает кромку ровно настолько, насколько
персонаж за неё зашёл — геометрия и рендер согласованы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь проверил в SDLPoP v1.24: оригинал падает с той же позиции,
по той же траектории и с тем же видом кадра падения (голова/руки поверх
кромки пола). Кадры совпадают один в один.
Механика записана с числами: у стоек с мечом weight_x = 13-14 против 3 у
обычной стойки, поэтому при взгляде влево точка веса уезжает на 13 px
вправо от Char.x, и кромку персонаж переступает раньше, чем выглядит.
Замер: x=151 -> dx_weight=164 -> колонка 7 (дыра) вместо 6 (пол).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg003:0430 «If char is holding sword, it makes redraw-area bigger»:
при Char.sword >= sword_2_drawn футпринт расширяется на одну колонку в
сторону взгляда (вправо при dir>=0, влево иначе). У нас char_footprint
этого не делал, и клинок торчал на колонку дальше области, где
перерисовываются передние грани: меч оставался поверх столба, а его след
— на фоне.
char_footprint получил параметр sword; pop_fore_over_char — тоже (страж
машет мечом ровно так же). Ветка Кида берёт Kid.sword, ветка стража —
Guard.sword.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame. Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.
Оригинал зовёт load_fram_det_col() (seg006:0144) сразу после
loadkid/loadshad и ДО control() — play_kid_frame (seg000:1211) и
play_guard_frame (seg000:1248). У нас этого не было.
Замерено брейкпоинтом на pop_jump_up_seq: Kid x=156 col=6 кадр 15
(dx=0 weight_x=3) при кадре стража image17 (dx=-1 weight_x=8) дал
curr_col=7 и distance=2 вместо 6 и 10 — то есть jump_up_plain (вернулось
A=28, пустой прыжок) вместо «шаг назад на x=160 + зацеп». Предсказание
по кадру стража совпало с намеренным до единицы.
Отсюда же плавающее поведение: кадр стража меняется каждый тик, distance
Кида скакал через порог 6 — то прыжок, то попытка зацепа с неверной X.
И «голова Кида поверх плиты (1,6)» — не баг отрисовки, а следствие позы,
которой в оригинале в этом месте не бывает.
Фикс: pop_load_fram_det_col() (pop_kid.c) + вызовы в pop_ctrl_tick и
pop_guard_tick. determine_col — только на ветке Кида: у нас он
существует лишь для него (pop_map работает с Kid, а не с Char).
Разбор с числами — bug_closed.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порядок работ по решению пользователя: L2-PASS -> L3-CHOMP/SKEL/CHKP.
Причина техническая — чомперы лягут в банк 2, где живёт отрисовка, и
чинить баги фона поверх свежей механики дороже.
TASKS: запись L2-PASS с картой уровня 2, снятой с res2002.bin —
стражи (5, комнаты 4/7/11/15/24), ловушки и зелья по комнатам, и
декодированные из LINKLOC/LINKMAP цепочки «кнопка -> что открывает»
(в т.ч. кнопка к.9 @1,1, открывающая дверь выхода в к.23). Плюс
отдельный список того, что сделано именно в L2 и на уровне 1 не
проверялось: большая склянка, меч с начала уровня, выход через дверь,
респавн на своём уровне.
bug_list: заведён раздел «Уровень 2» под список багов отрисовки,
который пользователь подаст отдельно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Palace (уровни 4+) отложен решением 2026-08-04. На доску вынесены три
задачи уровня 3, снятые с данных и SDLPoP, а не с общих соображений:
- L3-CHOMP: 5 чомперов (комнаты 5, 16×3, 22); шаблон как у пик/ворот,
риск — банк 2 занят на 86.5%.
- L3-SKEL: в данных уровня 3 стражей НЕТ ВООБЩЕ; единственный враг —
скелет, и он спецсобытие check_skel (seg002:1044), а не страж из
данных. Нужен новый атлас (data/SKEL, 29 файлов) — pop_pack_guard.py
прибит к GUARD/.
- L3-CHKP: чекпойнт (seg002:519 + seg003:141); hitp_beg_lev уже есть.
Плюс инвентарь тайлов по уровням: ур. 3 вводит только chomper(18),
palace-набор (lattice*) начинается с уровня 4 — отсюда и граница скоупа.
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>
BUG-SEAM-DRAW-1: Кид упирается в решётку и встаёт на x=61, curr_col=-1,
room=1 — и в комнате 1 РИСУЕТСЯ, за решёткой. При x=61 он уже внутри
системы координат комнаты 1 (obj_x = 6), straddle-смещение не требуется.
Остаток теоретический: при x <= 60 спрайт ушёл бы за левую кромку; полное
лекарство — довести S3, но поводов нет, заводить обратно только по живому
наблюдению.
BUG-GATE-PASS-1: однократное наблюдение — Кид стоял НА тайле решётки (0,9)
комнаты 5, дождался закрытия, пошёл вправо и прошёл в комнату 1. Повторить
не удалось: в том же месте при x=196/col=9 решётка держит штатно.
Плоскость блокировки для колонки 9 — x=205, а «колонка 9» по m7 это
x ∈ [191,205), то есть при curr_col==9 проход невозможен по построению;
значит наблюдался x >= 205, и поведение может оказаться штатным (решётка
закрылась за спиной). Гипотеза НЕ подтверждена, поэтому баг оставлен
открытым со списком того, что снять в следующий раз, и с двумя запасными
кандидатами на корень.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Падающий кусок теперь привязан к своей комнате и долетает после ухода Кида;
подтверждено живым прогоном. Автоматикой гонка не воспроизводилась — мост
MAME шлёт нажатия рывками, и «уйти раньше, чем долетит плита» через него не
набиралось; это отмечено в записи вместе с указанием, что кейс стоит первым
в плане host-тестов.
Вторая волна прогона уровня 1 закрыта целиком: BUG-KBD-4, BUG-RESPAWN-2,
BUG-DRAWORDER-1, BUG-LOOSE-2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сверено в SDLPoP: Кид, стоящий колонкой правее лежащего трупа, и там
рисуется поверх его головы. Значит порт верен, а артефакт врождённый:
set_objtile_at_char приписывает персонажа РОВНО ОДНОМУ тайлу, ширина
спрайта на выбор тайла не влияет, и всё, что стоит правее, перекрывает
выступающую часть тела. Механизма «широкий объект в нескольких тайлах» в
оригинале нет — сверено с draw_objtable_items_at_tile, sort_curr_objs и
веткой tile_object_redraw == 0xFF (та про оверлеи пола).
Закрытая запись перечисляет все четыре корня, которые пришлось снять по
очереди: порядок по роли вместо тайла; своя регрессия с общим окном
fore-клипа; колонка трупа из тайла вместо X; непортированная ветка
actions_1_run_jump. Записано и то, как отличать остаток от этих багов,
и цена «починки» остатка (тайл по центру габарита = отход от эталона).
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>
Одиннадцать наблюдений первого прогона свелись к шести корням, четыре
наблюдения второго — ещё к четырём. Разбор каждого — bug_closed.md.
Первая волна:
- BUG-LVLSTATE-1: уровень стал мутабельным (эталонная копия foretable для
рестарта, pop_level_set_tile вместо таблицы оверрайдов);
- BUG-RESPAWN-1: рестарт = load_level, тайлы возвращаются из эталона;
- BUG-DEATH-1: смерть от меча доигрывается (порт control_kid, seg006:0CD1);
- BUG-GATE-ANIM-1: ворота в отрисованной комнате перерисовываются
(POP_RD_GATE, порт draw_trob seg007:01E6);
- BUG-COLL-1: полный порт check_collisions/bumped (seg004) вместо поиска
стены только в колонке переднего края;
- BUG-STANDUP-1: убран лишний guard в bumped_floor — вставание у стены
роняло Кида сквозь пол.
Вторая волна:
- BUG-RESPAWN-2: рестарт возвращает и СТРАЖЕЙ (в оригинале play_level на
каждой итерации делает load_level + pos_guards);
- BUG-LOOSE-2: падающий кусок привязан к своей комнате и долетает после
ухода Кида (do_mobs крутит mobs[] независимо от drawn_room);
- BUG-DRAWORDER-1: порядок «Кид / страж» задаётся обходом тайлов
(redraw_needed_tiles: ряды 2,1,0, колонки 0..9), а не ролью персонажа.
По BUG-DRAWORDER-1 понадобилось три захода, и два первых были неполны:
1) сам порядок — но общее окно fore-клипа осталось стражьим, и Кид
нарисовался поверх передних столбов (kid_fore_clip_restore);
2) enter_guard брал curr_col из тайла, а leave_guard пишет туда
get_tilepos(0,row) — у запомненного ТРУПА колонка была 0 при
настоящей X. Теперь колонка выводится из X, как в оригинале;
3) ветка actions_1_run_jump в set_objtile_at_char оказалась не
«упрощаемой»: в беге тайл берётся из нижнего ряда и ЛЕВОЙ колонки
габарита, поэтому бегущий Кид уходит за объекты справа. Считается
для обоих персонажей — enter_guard ставит action=1 и стражу.
Проверено в MAME: зелья/меч/плиты переживают выход из комнаты и
восстанавливаются после смерти; кнопка room5 поднимает решётку; падение с
кнопки больше не роняет в комнату 6; убитый страж жив после respawn;
Кид проходит за телом стража. make size-check — роста нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (нашёл пользователь сразу после L1-EXIT): при подъёме по лестнице за
дверью уровня силуэт Кида вылезал ПРАВЕЕ правого косяка проёма; по высоте
обрезка была корректна.
Причина — недопортированная половина clip_char (seg006:1231). Для кадров
двери оригинал ставит ДВА клипа, у нас был только первый:
obj_clip_top = leveldoor_ybottom + 1; // было
obj_clip_right = leveldoor_right; // не было
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228 — портировано по коду.
Fore-слоем это не лечится: створка и косяк уходят в оригинале целиком в
backtable (draw_leveldoor, все add_backtable), рисуются ПОД персонажем и
перекрыть его не могут. Единственный способ — срезать сам спрайт.
libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у
колоночного блита. Для column-major это ровно уменьшение числа колонок, то
есть внутри ядра механизм уже был (так же клипается край экрана,
w = _bgi_maxx + 1 - x), наружу не выводился. Тело блита переехало туда,
gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким
gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part. Работает и при flip:
первые maxw нарисованных колонок всегда ложатся в левую часть футпринта.
make size-check: роста нет.
PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов)
пишет draw_leveldoor в pop_state — их читает clip_char из другого банка;
pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит
эти кадры с noclip-пути на общий. Прямоугольник heal (kid_lw) сужается тоже
— стираем ровно нарисованное.
Проверено в MAME: pop_leveldoor_right = 176, что есть ровно (draw_xh<<3)+48
для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у
поднятой — сходится с формулой оригинала. Отрисовку подтвердил пользователь
на живом подъёме.
Заодно: ROOMNAV остаётся включённым осознанно — это наш чит, которого в
оригинале не было, как и S/K/I; позже сведём в общий блок читов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L1-START. Старт и оба рестарта (смерть, выпадение из уровня) сведены в
pop_start_level() — порт start_level + do_startpos + set_start_pos (seg003).
Комната/тайл/направление берутся из pop_level_start_*, направление
инвертируется (~start_dir), поза входа — из tbl_entry_pose: у уровня 1 это
падение внутрь (seq_7_fall) плюс нажатие кнопки room5(0,2), то самое, что
захлопывает решётку за спиной. Жёсткие START_ROOM/COL/ROW убраны.
Проверено в MAME: старт даёт room 1, col 0, падение на row 1 — как по данным.
L1-EXIT. Ветка двери уровня из up_pressed + go_up_leveldoor (seg005:0482/
0574): тайлы и геометрия — pop_leveldoor_enter() в pop_map, последовательность
seq_70 — в pop_ctrl. Опкод 0xF1 END_LEVEL в play_seq инкрементит
pop_next_level (порт next_level), главный цикл по нему перезапускает уровень
— ровно та точка, куда levels_plan §2.2 подключит загрузку уровня 2.
Открытость двери проверяется по modifier >= 42 (ветка fix_exit_door), а не по
ванильному leveldoor_open: иначе можно войти в ещё ползущую створку.
Отдельно стоило разбора: go_up_leveldoor сначала писал Char.x/Char.direction,
и оба присваивания молча терялись — окно Char вокруг диспетчера возвращает
назад только curr_seq и sword (pop_savekid_state). Направление оставалось
«вправо», а все DX в seq_70 отрицательные, поэтому Кид уходил ИЗ проёма
влево (поймано стоп-кадром). Геометрию персонажа в этом порте меняет
pop_map, пишет в Kid — как pop_down_action и pop_jump_up_seq.
Известный остаток — BUG-DOOR-CLIP в bug_list.md: нет обрезки силуэта правым
косяком проёма (недопортирован obj_clip_right в clip_char); нужен вариант
колоночного блита с ограничением ширины. ROOMNAV пока оставлен включённым —
он нужен, чтобы попадать в комнату 9 для этой работы.
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>
Ручная проверка пользователем: стало значительно лучше, но редкие пропуски
стрелок при зажатом Shift всё же ощущаются. Счётчики на 35 нажатиях подряд
потерь не показали, то есть остаточная частота заметно ниже прежних ~15 %.
Задача отложена до финальной полировки программы (решение пользователя) —
для работы клавиатура пригодна.
Записано, где именно осталась дыра, чтобы не начинать с нуля: idle-хук
покрывает простой (~2/3 кадра), а в занятой трети DI-окно одного
accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс — пачка байт,
целиком попавшая в такое окно, ещё может потерять байт. Порядок действий
на возврат: вызовы между блитами занятой фазы, замер тем же счётным методом
от 50 нажатий, и только потом рычаги вне нашего кода (Scan Code Set 3 через
BIOS $EA — в MAME непроверяемо; общий m_irq_off_timer в драйвере).
Заодно сняты оговорки «плотный опрос ещё не подтверждён замером» в
kbd_raw.h и libc-reference.md — теперь там штатный рецепт через
gfx_set_idle_hook.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Причина потерь (замеры — applications/PoP/roomtest/TASKS.md, KBD-1): при
зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», нажатие
стрелки становится 5 байтами вместо 2, а импульс запроса прерывания здесь
теряется примерно в 44 % случаев — трёхбайтовый FIFO SIO переполняется, и
байт пропадает ДО чтения порта. Лечится только плотным вычерпыванием: раз
в ~0.5 мс. Столько времени есть даром — при пейсинге «3 растровых кадра на
логический тик» процессор проводит ~42 мс из 60 в gfx_wait_vsync, крутя
опрос луча и больше ничего не делая.
- gfx_set_idle_hook(fn) — что вызывать, пока gfx_wait_vsync ждёт луч.
Состояние в отдельном data-модуле (_gfx_idle_state.c), чтобы не тянуть
сеттер в программы, которые хук не ставят.
- Лучевой цикл зовёт хук в обеих фазах. BC (счётчик таймаута)
сохраняется, косвенный вызов — push адреса возврата + jp (hl), так как
`call (hl)` в Z80 нет; без хука это ret по нулевому указателю, порядка
двух десятков тактов в цикле, который и так сжигает время.
- Путь FPS-делителя не затронут: там ожидание через HALT.
- roomtest вешает на хук kbd_raw_poll.
Проверка в MAME счётчиками (брейкпоинты с { b@ADDR = b@ADDR+1 ; g } на
чтении порта 0x18 и на установке make-бита): 35 нажатий Shift+Home → 35
make, ноль потерь; до фикса было 9 из 10. Боевой сценарий: четыре Shift+→
подряд дали четыре осторожных шага (Kid.x 114 -> 147). _CODE +170 Б,
кадровый бюджет не затронут.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом: при удерживаемом Shift часть нажатий стрелок не отрабатывает
(~15 % по наблюдению пользователя), без Shift потерь нет.
Переведено в числа: нажимается Home — тоже расширенная клавиша (тот же
E0-префикс и тот же «fake shift»), но игрой игнорируется, поэтому рельеф
комнаты на результат не влияет. Счётчики — брейкпоинты MAME с действием
{ b@ADDR = b@ADDR+1 ; g } на входе клавиатурной ветки трамплина, на чтении
порта 0x18 и на установке make-бита.
Что измерено (10 нажатий Shift+Home, дошло make):
игра идёт, опрос ВКЛ 9/10 игра идёт, опрос ВЫКЛ 9/10
игра ЗАМОРОЖЕНА (блитов нет вообще, длинных DI нет) 8/10
Обе исходные гипотезы отпали:
- длина наших DI-окон ни при чём (в замороженном кадре потерь больше);
- снятие di в accel-ядрах libbgi УРОНИЛО машину — режим «акселератор при
EI» из docs/new/06-accel.md §6.6 в этой прошивке недоступен.
Байт теряется НИЖЕ нашего кода: на 49 прочитанных байт пришлось только 28
входов в клавиатурную ветку, то есть ~44 % импульсов запроса прерывания не
обслуживается и трёхбайтовый FIFO SIO переполняется.
Потолок приёма измерен отдельной программой tests/kbdpoll (ничего, кроме
kbd_raw_poll в цикле): 25 нажатий -> 25 make, 250 байт из 250, ноль потерь.
Значит опрос лечит полностью, вопрос только в плотности: нужно раз в
~0.5 мс, а шесть вызовов за 60-мс кадр давали раз в 10 мс.
Поэтому вызовы из roomtest.c УБРАНЫ (они стояли там, где прерывания и так
разрешены, и дублировали трамплин — 9/10 с ними и без). Сама функция
kbd_raw_poll оставлена в libc: она корректна и нужна как основа плотного
опроса. В заголовке и в libc-reference — честная оговорка, чтобы её не
ставили в игровой цикл «на всякий случай» без замера.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>