Files
Sprinter-SDCC/applications/PoP/roomtest/bug_list.md
T
snark13 8beb66a48d FORE-DUP: передний слой рисуется дважды при перекрытии — заведено с разбором
Найдено вопросом пользователя.  У нас fore-проход зовёт каждый рисующий по
своему прямоугольнику, поэтому тайл, накрытый двумя объектами, рисуется
два раза (Кид+отражение — всегда, Кид+соперник — весь ближний бой).

В оригинале дубль невозможен по построению: redraw_at_char (seg003:0427)
только ПОМЕЧАЕТ тайлы, а set_redraw_fore (seg007:0550) —
redraw_frames_fore[tilepos] = frames, присваивание.  Единственный обход
тайлов рисует передний кусок один раз.

Картинку не портит (прозрачный блит идемпотентен), тратит такты в самом
горячем месте.  Запись требует СНАЧАЛА замера: окно клипа вместо перебора
тайлов в своё время дало 3.2x, и переход на пометки может часть вернуть.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:22:48 +03:00

30 KiB
Raw Blame History

roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации

Только то, что не закрыто. Всё закрытое (и, что важнее, разбор корней) переехало в bug_closed.md: прежде чем заводить новый баг, грепни там по симптому.

Приоритеты работ — в TASKS_OPEN.md (закрытые задачи с протоколами — TASKS_CLOSED.md), а не здесь. Правило проекта: механику сверять с ../SDLPoP/src/ ДО кодинга.

Ревизия списка: 2026-08-07 — прогон ВСЕХ комнат уровней 1 и 2 (пользователь). Крупных багов нет. Поэтому в bug_closed.md уехали разом: чек-листы ручной перепроверки фиксов (2026-08-03, обе волны), таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений того же прогона — все они закрыты этим проходом. С прогона пришли три новые записи, все по уровню 2 (сырые формулировки — bugs_level2.md).

ID что тип статус
BUG-CHEAT-FIGHT-1 +/ в бою с вынутым мечом → Кид теряет управление Major (чит) ЗАКРЫТ 2026-08-10bug_closed.md
BUG-SPIKE-1 пики залипают выдвинутыми рядом с Кидом низкий маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается
BUG-CHOMP-JUMP-1 прыжок с места вплотную к чомперу: кадр с отступом назад низкий маловоспроизводимый: на повторе не поднялся; на пререлиз
T-1 пики перерисовываются безусловно оптимизация открыт
T-2 Кид перерисовывается в покое оптимизация ЗАКРЫТ 2026-08-08DRAW-COST шаг 1
BUG-GATE-PASS-1 проход сквозь закрытую решётку шва Major ЗАКРЫТ 2026-08-09bug_closed.md, смоук уровня 1 пройден
BUG-GUARD-IX-1 «зависание» в бою со стражем: затёрт IX главного цикла Blocker ЗАКРЫТ 2026-08-10bug_closed.md
BUG-GATE-SEAM-ROW1 решётка в шве не анимируется, если ворота не в ряду 0 Major ЗАКРЫТ 2026-08-10bug_closed.md
BUG-LOOSE-BUTTON-1 упавшая плита не нажимает кнопку под собой Major ЗАКРЫТ 2026-08-10bug_closed.md
BUG-GATE-FF-1 ворота «открыты навсегда» непроходимы (0xFF перегружен) Major ЗАКРЫТ 2026-08-10bug_closed.md
BUG-TORCH-CHOMP-1 чомпер стирает пламя соседнего факела Minor ЗАКРЫТ 2026-08-10bug_closed.md
DIED-ON-BUTTON died_on_button (seg007:776) не портирован порт открыт

Уровень 2 — баги с приёмки

Приёмка уровня 2 закрыта (L2-PASS): smoke 2026-08-05 + обход всех комнат 2026-08-07. Карта содержимого уровня (что где стоит по res2002.bin, какие кнопки какие ворота открывают) — там же, по ней видно, «механика не сработала» это или «так и задумано».

Закрыто с этой волны: BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком; BUG-GUARD-DEAF-1 — страж не оборачивался на вернувшегося Кида; BUG-GUARD-SPLASH-1 — «брызги» при попадании по стражу; BUG-SWORD-GHOST-1 — меч, спрятанный посреди боя после перехода комнаты (корень — мнимая стена за краем комнаты в get_tile; кэш соседей расширен до полных комнат); BUG-GUARD-COLOR-1 — цвет стража и его полосы HP теперь берётся из guards_color уровня (2026-08-07).

Ниже — то, что осталось открытым после прогона 2026-08-07.

BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ

Статус 2026-08-05, вечер. Пользователь повторить не смог, а замер (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай — пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в оригинале. Запись оставлена открытой ровно из-за визуального расхождения со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.

Как получить состояние нарочно: подойти к пикам вплотную (выдвинутся), отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним. Пользователь пробовал и это, и попиксельную подгонку X читом [/] — не поднялось. Вывод для будущего разбора: состояние не чисто позиционное, одной шириной габарита его не объяснить; следующий подозреваемый — момент, в который process_trobs застаёт модификатор относительно кадра Кида.

Наблюдение (пользователь, 2026-08-05). Уровень 2, комната 6, пики (1,3):

действие что происходит
длинный прыжок с ряда 0 на пики смерть — правильно (путь fell_on_spikes)
пробег по ряду 1 прямо по пикам урона нет
после уборки пик на экране остаются белые остатки остриёв (в оригинале чисто)
прыжок на месте, стоя на пиках урона нет
просто стоять на выдвинутых пиках можно сколько угодно

Замер (MAME, чтение room_modif комнаты 6). Пока Кид стоит на тайле, модификатор пики (индекс 13) = 0x8E, то есть «пики ПОЛНОСТЬЮ вышли и идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:

is_spike_harmful (seg007:1178):  0/-1 → 0;  <0 → 1;  1..4 → 2;  >=5 → 0
check_spiked     (seg006:0658):  убивает при h>=2 на кадрах бега 7..14
                                 и при h!=0 на кадрах приземления 43/26

То есть при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале — смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши animate_spike, start_anim_spike, is_spike_harmful, check_spiked сверены с seg006/seg007 построчно и совпадают дословно.

ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом). Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и «спрятались не все — часть артефактов осталась». Скриншоты рядом: наш и SDLPoP в той же позе. У нас из-под щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.

ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер 2026-08-05, MAME, watchpoint на room_modif[13] комнаты 6). Чистый пробег по УБРАННЫМ пикам убивает штатно:

запись modif=1 : кадр 11 (беговой), x=112, curr_col=2   ← пики пошли вверх
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3   ← Кид уже НА тайле, h=2
запись modif=3 : кадр 177 (frame_177_spiked)            ← напоролся

То есть check_spike_below, check_spiked, is_spike_harmful и тайминг выдвижения работают правильно, и «раннего» триггера нет.

Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми. Пока габарит Кида накрывает колонку пики, check_spike_below каждый кадр зовёт start_anim_spike, а тот при отрицательном модификаторе переставляет его обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1) для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег по уже вышедшим пикам не убивает, и они же остаются торчать на экране.

Что осталось выяснить (и это единственный открытый вопрос). Код start_anim_spike у нас с оригиналом совпадает дословно, значит оригинал тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания площадки SDLPoP уводит Кида на 134, мы — на 141).

Как закрывать: инструментировать SDLPoP (печать char_x_left/right, left/right_checked_col и модификатора пики каждый кадр), проиграть ту же сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а диапазоны колонок разойдутся — виноват габарит (kid_fp против set_char_collision текущего кадра); если разойдутся позиции — это отдельный баг посадки, а пики — его следствие.


Уровень 3 — не баги, а неначатые задачи

Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два были настоящими багами и закрыты в тот же день (разбор — в bug_closed.md: BUG-JUMPWALL-1 и BUG-SEAM-WEDGE-1). Оставшиеся два — не баги, а неначатые задачи:

наблюдение что это на самом деле
к.22: чомпер (2,6) не анимируется и вообще не рисуется L3-CHOMP — механики чомперов НЕТ. В таблице тайлов pop_bg.c:211 строка 12 chomper рисует только основание, правую грань и низ; самих челюстей (спрайт из chtab, draw_tile_anim seg008) нет вовсе. Так и должно выглядеть до порта
к.10: скелет не оживает L3-SKELcheck_skel (seg002:0E1F) не портирован. И комната другая: тайл skeleton(21), который оживает, лежит в к.1 (1,5) — это skeleton_room=1, skeleton_column=5, skeleton_row=1. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда дверь уровня уже открыта и Кид стоит в колонке 2 или 3

Приёмки уровня 3 (полного обхода комнат) ещё не было — она осмысленна только после L3-CHOMP/L3-SKEL.


Оптимизация отрисовки (записано 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, отдельно не окупается.

T-2. Idle-skip — ЗАКРЫТ 2026-08-08

Сделан как шаг 1 задачи DRAW-COST (коммит a25ce58), и шире, чем формулировался здесь: пропускается не только Кид, а ЛЮБОЙ персонаж, у которого с прошлой отрисовки этой страницы дабл-буфера не изменился ни один вход отрисовки, — включая труп стража и ждущего стража. Условие «обе страницы уже получили это состояние», которого требовала эта запись, выполнено само собой: снимок входов хранится ПО СТРАНИЦАМ.

Замер: комната 1.3 с трупом стража, Кид стоит — 210 % -> 116 % кадрового периода, ноль вызовов pop_heal_fast за кадр. Контракт — шапка pop_cdraw.h, разбор и что делать дальше — TASKS_OPEN.md#draw-cost.

Связь с T-1 сработала как и предсказано: пока Кида не перерисовываем, heal'а нет, стирать пики нечем. Но T-1 остаётся открытым — трогать пики безусловно мы всё ещё продолжаем, когда Кид рядом ДВИЖЕТСЯ.


BUG-CHOMP-JUMP-1. Прыжок с места вплотную к чомперу: кадр с отступом назад — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ

Статус 2026-08-08. Наблюдение пользователя на приёмке L3-CHOMP: Кид стоит вплотную СЛЕВА от чомпера лицом вправо, прыжок с места — в анимации проскакивает кадр, где он «чуть отступил назад», и только потом идёт прыжок. В оригинале прыжок идёт прямо с места. На повторе в тот же заход не воспроизвёлся — отложено до пререлиза.

Что уже установлено (чтобы не разбирать заново).

  1. Это НЕ анимация. seq_3_standing_jump (SDLPoP seqtbl.c:382) состоит только из положительных смещений: act(run_jump), f16, f17, dx(2) f18…f22, dx(7) f23, dx(9) f24, dx(5) dy(-6) f25. Ни одного отрицательного dx — отката в последовательности нет вовсе. Значит отступ даёт bumped(), то есть физика посчитала въезд в препятствие и выровняла Char.x назад.

  2. is_obstacle для чомпера у нас совпадает с оригиналом (seg004:037E): препятствие только при modif == 2, причём именно == 2, БЕЗ маски 0x7F — то есть окровавленный чомпер (0x82) в ванили не бампит вовсе. Проверено, расхождения нет.

  3. Прямая трасса прыжка отката НЕ показала. Ввод «UP и RIGHT одновременно» (mame bridge, key UP 24 + key RIGHT 24) даёт чистое движение вперёд: x 148 -> 155, кадр становится 178 (перемололо). Ни одного кадра с уменьшением x.

Главная гипотеза — ПОРЯДОК НАЖАТИЙ. Если ↑ приходит на кадр раньше →, то control_standing уходит не в standing_jump(), а в up_pressed() — вертикальный прыжок с зацепом, а он выравнивает Char.x. Отсюда и «отступил, потом прыгнул». Проверять надо check_jump_up / jump_up_or_grab / grab_up_no_floor_behind (seg005:0836 и далее), а не прыжок с места. Внимание на can_climb_up (seg005:0828): там у чомпера ЕСТЬ спецкейс (seq_73_climb_up_to_closed_gate при взгляде ВПРАВО) — он у нас портирован (pop_map.c, ветка TILE_MIRROR || TILE_CHOMPER), но именно вокруг него и стоит копать.

Как ловить. Брейкпоинт на резидентном _kid_tick с {printf "f=%d x=%d act=%d col=%d",b@Kid+0,b@Kid+1,b@Kid+6,b@Kid+4; g} — одна строка на кадр, адрес _Kid из .sprinter-cc-roomtest/roomtest.map (после КАЖДОЙ пересборки другой). Плюс стоп-кадр 1 в момент отступа и чтение Kid из памяти. Метод — memory z80_profiling_method.


Заметки (отладка)

  • Тестовые клавиши осторожного шага: J = шаг влево, L = шаг вправо (эмуляция Shift+стрелка), см. pop_ctrl.c KBD_DBG_STEP*. Первый шаг в сторону = разворот (как в оригинале safe_step), движение со второго.
  • Читы (pop_cheat.h): K — убить стража, I — бессмертие (toggle), S — выдать меч, Shift+L — следующий уровень, [/] — сдвиг Кида на пиксель по X.
  • Респавн после смерти — по (или авто через RESPAWN_DELAY); ставит Kid в стартовую позицию УРОВНЯ (pop_start_level, порт do_startpos).
  • ROOMNAV (=/-) — тоже наш чит, которого в оригинале не было, как и S. Все они со временем съедутся в общий блок читов, разрешаемый в настройках; пока просто включены (pop_cheats = 1 в roomtest.c). Известный баг этого чита закрыт — BUG-CHEAT-FIGHT-1.
  • Комнаты 13, 18, 24 уровня 1 недостижимы в обычной игре — это свойство данных уровня (разбор — «НЕ БАГИ» в bug_closed.md); приоритет багов в них низкий. Аналогично 23/24 на уровне 3.

DIED-ON-BUTTON. died_on_button (seg007:776) не портирован

Обнаружено при разборе BUG-LOOSE-BUTTON-1. check_press (seg006:1707) разбирает ЛЮБОГО мёртвого Char, не только Кида:

if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
    if (Char.alive < 0) trigger_button(1, 0, -1);   /* жив */
    else                died_on_button();           /* мёртв */
}

died_on_button на opener делает тайл обычным полом и форсирует button_type = tiles_14_debris — ворота открываются насовсем; на любой другой кнопке ставит tiles_5_stuck (заклинена, link_timer == 0x1F, связь мертва).

У Кида эффект живёт до перезапуска уровня (смерть → is_restart_levelplay_level заново зовёт load_level(), тайлы перечитываются), а вот когда на кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.

Что нужно: сам порт died_on_button, константа TILE_STUCK = 5 и тайл заклиненной кнопки в атласе фона (сейчас его там нет).


TORCH-ANIM-RIGHT. Под пламенем могут застыть не только челюсти чомпера

Открыто 2026-08-10 при закрытии BUG-TORCH-CHOMP-2.

Пламя факела запекается в фон и рисуется в ячейке ПРАВОГО СОСЕДА. Оригинал после каждого кадра факела метит этого соседа (set_redraw_anim_right, seg007:0101) и перерисовывает весь его слой anim поверх огня; мы метим только когда сосед — чомпер. Слой draw_tile_anim (seg008:0644) рисует также пики, зелье и меч — если такой тайл окажется справа от факела и будет в статике, пламя накроет и его.

На уровнях 1–4 такого соседства не встретилось, поэтому расширять пометку (она стоит перерисовки тайла каждый кадр на каждый факел) заранее не стали.

Как проверять: поставить факел слева от пики/меча/зелья в тестовой комнате и дождаться статики; либо пройти уровни 5+ и смотреть на клетку справа от каждого факела.

Как чинить, если встретится: там же, в ветке факела pop_process_trobs, добавить в trob_rcode-проверку нужные коды и соответствующий вид пометки (POP_RD_SPIKE / POP_RD_FLOOR), а зелье — переставить в порядке обхода так, чтобы оно рисовалось ПОСЛЕ факела.


FORE-DUP. Передний слой тайла рисуется ДВАЖДЫ при перекрытии объектов

Найдено пользователем 2026-08-10 (вопросом, а не по симптому — картинка верная, страдают только такты).

У нас. Fore-проход зовёт КАЖДЫЙ рисующий по своему прямоугольнику: pop_char_fore для слотов Кида и соперника, pop_mirror_draw для отражения. Окно клипа (pop_fore_set_clip) у каждого своё. Если футпринты двух объектов накрывают один тайл, его передний кусок рисуется два раза.

Когда случается:

  • Кид и отражение — ВСЕГДА (стоят на одном тайле зеркала); этот случай создан фиксом fore-прохода над отражением 2026-08-10;
  • Кид и соперник в одном тайле — то есть весь ближний бой;
  • Кид и падающий кусок плиты.

Картинку не портит: куски переднего слоя идут прозрачным блитом-копией, операция идемпотентная. Портило бы при XOR/mono с накоплением — таких в fore-слое нет.

Такты тратит, и в самом дорогом месте: fore-проход исторически самая тяжёлая часть кадра (memory pop_fore_layer_cost — было 78 % кадра, окно клипа и кэш кладки дали 3.2×).

Как устроено в оригинале — дубль НЕВОЗМОЖЕН по построению. redraw_at_char (seg003:0427) ничего не рисует, а только помечает тайлы своего прямоугольника:

for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
    for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col)
        set_redraw_fore(get_tilepos(tile_col, tile_row), 1);

а set_redraw_fore (seg007:0550) — это redraw_frames_fore[tilepos] = frames, ПРИСВАИВАНИЕ. Два персонажа на одном тайле оставят там ту же единицу, и единственный обход тайлов нарисует передний кусок один раз.

Что делать. Механизм пометок у нас уже есть и прямо назван портом этой архитектуры — pop_redraw.h. Fore-слой остался единственным местом на прямом вызове. Приведение к оригиналу: pop_char_fore/pop_mirror_draw вместо прохода ставят пометки, а один проход в конце кадра их разбирает.

СНАЧАЛА ЗАМЕРИТЬ, потом чинить. Это самый горячий путь, и окно клипа (вместо перебора тайлов) в своё время дало 3.2× — переход на пометки может часть этого вернуть назад. Замер: брейкпоинт на листьях fore-слоя со счётчиком, сцена «бой в комнате 3 уровня 1» и «Кид на тайле зеркала, ур. 4»; сравнить число нарисованных кусков с числом уникальных тайлов. Если дубль мал — оставить как есть и закрыть запись.