Files
Sprinter-SDCC/applications/PoP/roomtest/bug_list.md
T
snark13 08c6a8504f PoP roomtest: отладочный обход комнат (+/-) + TODO по редрою пик и idle-skip
ROOMNAV (одна строка #define в roomtest.c): '+'/'-' — следующая/предыдущая
комната уровня ПО НОМЕРУ (1..24, с обёрткой), Kid ставится на первый пол,
pop_trob_reset() возвращает пики/ворота в исходное.  Нужен для обхода всех
24 комнат в поиске багов отрисовки, включая недостижимые обычным путём.
Цена — 505 Б кода W1 (22816 -> 23321), они же вычитаются из кучи (4178 ->
3673 Б); W3 не тронут.  Переводить приложение в huge ради этого не стали:
смена модели памяти (банкованный W1 + трамплины) ради полукилобайта —
плохой размен прямо перед прогоном комнат.

Номер комнаты — ПОЛОСКАМИ в верхнем борте (слева десятки, справа единицы),
а не outtextxy: текст тянет системный знакогенератор (_gfx_font_buf, 2 КБ
статики) и сажает кучу до 576 Б.  Цвет полосок 0x57, а не WHITE: палитра
игровая, запись 15 в ней ЧЁРНАЯ (kid.pal 0x0F = 0,0,0) — из-за этого
закомментированный ранее лейбл был бы невидим в любом случае.

bug_list.md: раздел TODO (T-1 редрой пик по причине, T-2 idle-skip) +
пустая таблица на 24 комнаты под результаты обхода.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:58:06 +03:00

31 KiB
Raw Blame History

roomtest — список известных багов (Фаза B и смежное)

Найдено при тестировании Фазы B (кнопки/ворота) в MAME. НЕ решаем сейчас — задача следующего этапа. Правило проекта: механику сверять с applications/PoP/SDLPoP/src/ ДО кодинга.

Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (START_ROOM в roomtest.c). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом шве room6.


BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — Critical

Симптом: при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов) Kid оказывается на ряду row 1 вместо row 0 — «проваливается» на этаж ниже (стоит в дверном проёме на неправильной высоте).

Где: pop_map.c check_leave() (порт seg002 leave_room) — боковой уход pop_leave_dir; репроекция координат Kid.x ∓= 140 есть, но Y/curr_row при боковом переходе НЕ репроецируются (в отличие от check_leave_below, где Kid.y -= 189; curr_row = y_to_row). При входе в соседнюю комнату у шва Kid, видимо, попадает в позицию, где determine_col/физика следующего кадра сбрасывают его на row 1 (нет пола на row 0 у входа, либо неверный y).

Сверить с SDLPoP: goto_other_room (seg002) — как именно пересчитываются Char.curr_row/Char.y при side-переходе (left/right). Для вертикального перехода мы это сделали, для бокового — нет.

BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — Critical

Симптом: после перехода в соседнюю комнату попытка сразу вернуться обратно приводит к тому, что Kid снова закидывается в ту же комнату — переход «пинг-понгует», выйти нельзя.

Где: pop_map.c check_leave(). После перехода Kid репроецирован к ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (x ∓ 140). На СЛЕДУЮЩЕМ же кадре передний край снова за порогом ухода в обратную сторону → pop_leave_dir взводится опять → мгновенный обратный переход. Нужен guard «только что перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от кромки), как в оригинале (next_room/leave обрабатываются раз за кадр + Kid входит достаточно внутрь).

Сверить с SDLPoP: порядок leave_room/goto_other_room + на какой X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе мгновенный обратный триггер).

BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — Critical

Симптом: когда Kid подтягивается (climb-up) на тайл, верх которого — КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю часть Kid во время подъёма.

Где: pop_bg.c climb_overlay_tile() — floor-overlay при подъёме рисуется только для floor-подобных тайлов code ∈ {1,3,5,19}. Кнопка (opener 0x0F / closer 0x06) в этот набор НЕ входит → пол-оверлей кнопки не рисуется поверх Kid. Нужно: для нажатой кнопки (draw как floor/stuck) трактовать её как floor-тайл в climb-overlay (учесть подстановку из get_tile_to_draw), и/или добавить opener/closer в набор.

Сверить с SDLPoP: draw_floor_overlay (seg008) + get_tile_to_draw (нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по ПОДСТАВЛЕННОМУ типу тайла.


Оптимизации (отложено)

OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22

Возможность: pop_room_redraw_seam_left() (pop_bg.c) на каждое изменение openness рисует bar(BLACK) по всему col0 + полный draw_tile(0,0) (стены, topright, wall_pattern с prandom() — десятки блитов). Реально анимируются только бары решётки — draw_gate_back (~9 env_b).

Стоимость: seam-блок (синий io_border) занимает ~30-50% кадрового периода, но только пока openness меняется — т.е. во время открытия и медленного авто-закрытия (~5 сек после схода с кнопки). В покое (ворота открыты-держим кнопку / закрыты / FF) — 0%. Замерено в MAME: брейк на _pop_room_redraw_seam_left (0x5A54) срабатывает ⟺ сегмент дорогой.

Приём (как у пик, но сложнее — heal НЕ годится: печёные бары устаревшие, поэтому и стоит bar(BLACK)+redraw): bar(BLACK) только по полосе баров (x=0, gate_top..gate_bot) + draw_gate_back(lmod,...) + дорисовать статику тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%. Пиксель-чувствительно — полоса баров пересекается со статикой col0, обязательна выверка в MAME по кромкам.

Решение (2026-07-22): оставляем как есть — стоимость транзиентная, в бюджет помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами.


Исправлено

  • Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука срезана до одного пикселя — ИСПРАВЛЕНО 2026-07-28. Две причины: (а) не был портирован clip_char() (seg006:1749) — верхняя обрезка спрайта по y_clip[curr_row+1], когда тайл над головой стена/пол; сделано (pop_clip_char_top в pop_map.c + gfx_blit_cols_part в libbgi, heal чистит уже обрезанный прямоугольник); (б) climb_overlay_tile выбирал ветку draw_floor_overlay (seg008:1E3A) по СЫРОМУ коду тайла — а нажатая кнопка в get_tile_to_draw (seg008:240) подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в draw_other_overlay и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn() (одна подстановка на все слои). Проверено покадрово в MAME (кадры 148..138).

  • Шов ворот жёг 50% кадра в покое (закрытая решётка) — ИСПРАВЛЕНО 2026-07-22. Причина — баг кодогенератора SDCC z80 (memory sdcc_z80_cmp_store_a_bug): if (m[9] != seam_sig) seam_sig = m[9]; компилировался в sub (seam_sig) (A ← разность) + ld (seam_sig),a — сохранял РАЗНОСТЬ m[9]-seam_sig, не m[9]. seam_sig осциллировала (напр. 25↔231), m[9]!=seam_sig истинно каждый кадр → draw_tile(0,0) каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения (seam_sig = g; из чистого g ДО sub), подтверждён в .asm. Прочёс всех модулей PoP: других случайных compare-then-store нет.

  • Пики: 2×полный draw_tile на кадр анимации → хирургический редрой, 2026-07-22. pop_spike_redraw: heal_off уже возвращает всю печёную статику (база 127, пол, грани); поверх анимируются только два острия (SPIKES_FRAM_LEFT в своей ячейке + SPIKES_FRAM_RIGHT в соседней). Заменили 2×draw_tile на 2×env_b — пиксель-в-пиксель тот же результат, в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).

  • Отрисовка нажатой кнопки (0,2)/(0,3) — ИСПРАВЛЕНО. Причина: fore_tile (pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id 149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки». Фикс: fore_tile применяет ту же подстановку нажатой кнопки, что и draw_tile (opener→floor / closer→stuck при таймере связи >1). Плюс pop_button_redraw — wipe своей ячейки + правой грани (дальний угол в 0,3), низ строго yb+64 (не залезать в стену ряда 1).

Заметки

  • Тестовые клавиши осторожного шага: J = шаг влево, L = шаг вправо (эмуляция Shift+стрелка), см. pop_ctrl.c KBD_DBG_STEP*. Первый шаг в сторону = разворот (как в оригинале safe_step), движение со второго.
  • Респавн после смерти — по (или авто через RESPAWN_DELAY).
  • START_ROOM=6 — временный тестовый старт; вернуть на pop_level_start_room() после отладки объектов.

BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — Medium

Симптом (2026-07-22): при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок должен окклюдить руки (как передняя грань).

Где: pop_bg.c — полоса потолка (draw_tile(-1)) сейчас только фон (запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в pop_fore_over_kid (перерисовать срез полосы над Kid поверх спрайта), по аналогии с fore-тайлами. Инфраструктура g_clip_top/gfx_blit_part уже есть.

BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — Medium

Симптом (2026-07-22): loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается.

Где: верхний сосед не «живой» — above_fg берётся из статики уровня, loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся, P0). check_press/knock работают только по текущей комнате. Нужно: (а) пробой loose сверху через границу комнаты, (б) персистентное loose-состояние соседа + его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя).

BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — Medium

Симптом (2026-07-22): Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине), которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя.

Где: pop_bg.c pop_fore_over_kid/fore_tile — рисует fore футпринт-тайлов без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке).

BUG-CEIL-3. Анимация ворот стирает потолок над ними — Medium

Симптом (2026-07-22): при опускании/поднятии решётки шва (анимация openness) полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом).

Где: pop_bg.c pop_room_redraw_seam_leftpop_button_redraw/ pop_spike_redraw — общий класс): чёрный bar по col0 row0 стирает потолочную полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу. Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы (draw_tile(-1,col) с g_clip_top) над затронутыми колонками.


BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — РЕШЁН 2026-07-22

Статус: 2026-07-22, вечер. НАСТОЯЩИЙ корень найден потиковой трассой ЖИВОГО SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char + char_x_left/right; fixes выключены = vanilla). Все прежние гипотезы ниже оставлены для истории — они НЕ были причиной.

КОРЕНЬ (подтверждён трассой + исходником)

set_char_collision (seg006:0723): char_x_right = obj_x/2 + 58, где load_frame_to_obj (seg008:1728) считает obj_x = 2*char_dx_forward(dx) - 116 и добавляет +1 для кадров «чётного пикселя»: if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x; (бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит). Деление obj_x/2 — C-усечение К НУЛЮ, поэтому при e = x+dx <= 57 (obj_x < 0, зона левого шва) поправка +1 даёт char_x_right = e+1, а при e >= 58 формула сокращается к чистому e.

Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 — бит 0x80 не стоит), имеет char_x_right = 58 и порога leave-left (<=57) НЕ достигает. Наш движок считал передний край как Kid.x + dx без поправки → 57 → ложный leave → пинг-понг.

Эталонный цикл SDLPoP (сн normalized по трассе): стойка x=61 → тап вправо → safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 → seq47 dx(-4) → x=57 (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает; тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап → bump → 57 … по кругу. Char.room и drawn_room НЕ меняются вообще.

Фикс (pop_map.c)

char_x_forward_edge(): e = char_dx_forward(dx); if (((flags ^ (dir<0 ? 0x80 : 0)) & 0x80) == 0 && e <= 57) e++; — используется в char_front_coll (коллизия/bump/edge_distance) и в check_leave (порог ухода). Плюс порт doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого выхода нет). pop_leave_timer (exit_room_timer) оставлен — он реален в seg002/seg003.

Гипотезы, ОТПАВШИЕ после трассы (история диагноза)

Симптом (подтверждён пользователем + инструментально)

Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя состояниями:

  • A: показывается room6, Kid у левой кромки за решёткой (спрайт виден на 2/3);
  • B: показывается room8, Kid у его правой кромки.

Эталон SDLPoP (описан пользователем): drawn_room всегда остаётся room6, Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ переключается. То есть у нас лишний свитч A→B.

Тестовый контекст

START_ROOM=6. Ворота room8(0,9) закрыты, видны в col0 room6. Kid-структура kframe: см. pop_kid.h (frame,x,y,direction,curr_col,curr_row,action,...). Адрес _Kid и статиков — в .sprinter-cc-roomtest/roomtest.map (сдвигается от сборки к сборке). MAME: memory mame_mcp_bridge/mame_hdd_test_disk. Отладочные: J/L — короткий шаг влево/вправо (эмуляция Shift+стрелка).

Инструментальный диагноз (watchpoint на pop_leave_dir)

В момент лишнего свитча A→B: pop_leave_dir=1 (LEFT), Kid frame=15 (СТОЯ, не transient!), Kid.x=57 (до репроекции +140). То есть:

  • char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57.
  • Порог leave-left (взгляд вправо): char_x_right <= 57 → срабатывает РОВНО на 57.
  • Грань ворот (где их держит коллизия) = 61 (wall_dist_from_left[1]=10 + coll_tile_left_xpos=51). Между 57 и 61 — зазор 4px: Kid НЕ удержан воротами (d=6157=4≥0 → check_bumped не бампит), но уже на пороге ухода.
  • Kid оседает на 57 из-за recoil отскока: seq_47 = act(bumped), dx(-4), frame_50, 51, 52; SEQ_DX(4) двигает Char.x на −4 суммарно; frame_50.dx=4 компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку (frame15, dx=0) char_x_right = Char.x = aligned4 = 57.

Что ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)

  • Формула char_x: char_x_right = obj_x/2+58 = Char.x+frame.dx (seg006 set_char_collision) — совпадает с нашим char_dx_forward.
  • Позиция грани ворот: get_left_wall_xpos = wall_dist_from_left[1](10) + xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184140)+7 = 61 — совпадает с нашим (x_bump[1+5]=44, +7, +10 = 61).
  • Порог leave: SDLPoP leave_room looking-right char_x_right<=57 — совпадает.
  • Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15 (image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
  • seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.

Почему точечные фиксы НЕ работают

  • Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА, не SDLPoP (в leave_room такого нет); откачено.
  • exit_room_timer=2 (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасает: положение Char.x=57 устойчивое (Kid стоит), а не transient — после истечения таймера leave срабатывает.
  • По коду SDLPoP raw-рассуждением раз за разом выходит, что leave-left ДОЛЖЕН сработать и там (Char.x=57), но эталон показывает, что нет. Значит различие не в этих кусках, а в ДИНАМИКЕ: где именно оседает Char.x у шва И как вычисляется drawn_room.

КОРЕНЬ (гипотеза)

Наш движок держит drawn_room == комната_Kid ВСЕГДА, а шов подделан:

  • коллизия — самодельные снапшоты кромок соседа (g_lcol/g_rcol, 1 колонка) + x_bump[tcol=-1], а НЕ порт curr_row_coll_room[]/get_all_coll_row/ xpos_in_drawn_room из seg004;
  • уход — мгновенный enter_room(сосед) при пороге char_x, а НЕ отложенный drawn_room через next_room/check_the_end.

Из-за упрощённой коллизии Char.x у шва оседает иначе, чем в SDLPoP (у нас ровно 57 = порог), и/или drawn_room переключается там, где в оригинале нет.

План S2/S3 (порт coll_room + отложенный drawn_room) — НЕ потребовался для

этого бага (трасса показала: в эталоне Char.room/drawn_room вообще не

меняются). Остаётся как задел под стражей/двух персонажей в кадре.

Правило проекта: SDLPoP — источник истины, переводить 1:1, НЕ рассуждать «как должно быть». Нужно портировать связку целиком:

  1. Раздельные комнаты. Char.room (реальная комната персонажа) ≠ drawn_room (отрисованная). У нас уже есть kid_room (S1) + рендер-смещение pop_kid_set_render_dx(∓140) (порт xpos_in_drawn_room для СПРАЙТА).

  2. Коллизия по Char.room через coll_room (seg004, S2). Портировать check_collisionsget_row_collision_data (строки curr/below/above) → get_left_wall_xpos/get_right_wall_xpos (по xpos_in_drawn_room(x_bump[ col+FIRST_ONSCREEN_COLUMN]), ±140 когда колонка принадлежит соседу) → массивы curr_row_coll_room[]/_flags[], bump_col_left_of_wall/ bump_col_right_of_wallcheck_bumped_look_left/rightbumped(). Тайлы берутся из РЕАЛЬНОЙ комнаты колонки (get_tile(room,col,row) для любой комнаты), НЕ из снапшота. Именно это определяет, где Char.x оседает у шва.

  3. Отрисовка по drawn_room, персонаж со сдвигом. Рисуем drawn_room; Kid (и будущие стражи) — со смещением ±140, если их комната ≠ drawn_room.

  4. Отложенная смена drawn_room (seg002/seg000, S3). Уход = leave_room (seg002:0504, ПОРТ ЦЕЛИКОМ, включая up/down/side, guard'ы climb/standup/turn, doortop-check для right) → goto_other_room (seg002:0486: Char.room=сосед, x±140/y±189). exit_room (seg002:03C7): exit_room_timer + next_room = Char.room. check_the_end (seg000:0FBD): if (next_room != 0 && next_room != drawn_room) drawn_room = next_room; redraw. drawn_room меняется ТОЛЬКО так, а не по нашему прямому enter_room.

Ключевой момент, который надо переводить БУКВАЛЬНО (а не выводить): порядок вызовов в play_frame (seg000:863 play_frame: … play_kid_frame → exit_room → check_the_end) + set_char_collision перед leave_room в exit_room.

Карта сегментов: seg002 (leave/goto/exit_room), seg004 (коллизия/coll_room/ bumped), seg006 (set_char_collision/play_seq/frame_table_kid), seg000 (play_frame/check_the_end/draw). Наши файлы: pop_map.c (коллизия/физика/leave), roomtest.c (enter_room/drawn_room/главный цикл), pop_kid.c (кадры/рендер-сдвиг).

Реализовано на 2026-07-22: S1 (kid_room + render_dx) + pop_leave_timer. S2/S3 отложены — баг закрыт odd-pixel поправкой (см. КОРЕНЬ выше). Память: pop_seam_room_model, sdlpop_odd_pixel_char_x.


TODO: оптимизация отрисовки (записано 2026-07-29)

Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый кадр), а у нас каждая такая пометка превращается в реальный heal (копию из ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем безусловно» корректен, но дорог.

T-1. Пики: перерисовывать по причине, а не безусловно

Сейчас: pop_process_trobs зовёт pop_spike_redraw каждый кадр для каждой живой пики в комнате (порт redraw_21h, который animate_spike вызывает вне всяких if). Это корректно, но лишнее для пик, до которых Киду дела нет.

Надо: перерисовывать тайл пики, только если

  1. сменился её видимый кадр (шаг выдвижения/уборки), ЛИБО
  2. её кто-то стёр — а стереть у нас может только heal, то есть тайл попал в прямоугольник kid_heal этого кадра.

Это и есть модель оригинала, просто выраженная флагами: redraw_at_char (seg003:0576) каждый кадр помечает set_redraw_fore тайлы персонажа, причём объединение текущего и предыдущего прямоугольника (MIN(char_top_row, prev_char_top_row) и т.д.), а animate_spike помечает свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.

Как: слой Кида и так считает cL..cR/rT..rB в pop_fore_over_kid — пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов сравнивает tilepos с диапазоном целочисленно. Никаких пересечений прямоугольников (см. память manual_hints_over_auto_detect).

T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит

Сейчас: kid_healkid_drawpop_fore_over_kid идут каждый кадр, даже когда Kid стоит и в его тайлах ничего не меняется. Это ровно поведение оригинала (draw_game_frame, seg000:917 — draw_moving() + draw_tables() безусловно), но у него это дёшево, а у нас нет.

Надо: пропускать heal+draw Кида, когда кадр/поза/координаты не менялись и в его тайлах нет активной анимации.

Осторожно (дабл-буфер): пропускать можно не раньше второго подряд неизменного кадра — иначе одна из двух страниц останется со старым содержимым. Условие «обе страницы уже получили это состояние».

Связь с T-1: после T-1 idle-skip не требует ничего дополнительного для пик: раз Кида не перерисовываем — heal'а нет — стирать пики нечем — редрой не нужен, он отпадает сам.


Обход всех 24 комнат уровня 1 — поиск багов отрисовки

Инструмент: #define ROOMNAV в roomtest.c+/- (цифровой блок либо =/- основного ряда) переключают комнату по номеру (1..24, с обёрткой), Kid ставится на первый пол, pop_trob_reset() возвращает пики/ворота в исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа единицы (|| |||| = 24). Убирается закомментированием одной строки.

комната статус что не так
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24