Commit Graph

530 Commits

Author SHA1 Message Date
snark13 f47d79ded8 guard_over_kid — только когда кого-то рисуем: минус 14 118
Разложил остаток цианной фазы (44 007 на трёх вызовах):

  pop_loose_mob_draw        978   гейт mobs_live работает
  guard_over_kid         14 424   трамплин в банк 8 + два objtile_at_char
  pop_char_skip_mask     28 605   трамплин в банк 4 + два cd_quiet

guard_over_kid отвечает на вопрос «кто рисуется поверх кого», а он не имеет
смысла, когда не рисуется никто.  Перенёс вызов ПОСЛЕ pop_char_skip_mask и
сделал условным: при skip == 3 оба слота тихие, и порядок не нужен.

Перестановка безопасна: обе функции только читают, и читают разное —
skip_mask снимок cd_sig, guard_over_kid габариты pop_cd прошлого кадра.

Замер 11/15, лёгкая позиция: циан 55 257 -> 41 280, работа кадра
456 780 -> 442 662.

Проверено в MAME: статика чистая, в бою (Кид сближается и бьёт стража)
персонажи перекрываются правильно, порядок не сломался.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:42:54 +03:00
snark13 379c513087 cd_quiet: сравнение снимка без построения структуры — минус 8 016
cd_sig_make СТРОИТ структуру из тринадцати полей в стековом кадре (то есть
через -n(ix)), и только потом шёл побайтовый цикл сравнения.  А зовётся
проверка четыре раза за кадр: pop_char_skip_mask дважды, и в ней по два
слота.

Новый cd_sig_same сравнивает поля прямо с источником, с ранним выходом на
первом расхождении — у двигающегося персонажа это обычно первое же поле.
cd_sig_make остался: он нужен pop_char_draw, чтобы снимок записать.

Замер 11/15, лёгкая позиция: участок «mob_draw + guard_over_kid +
skip_mask» 47 883 -> 43 875, циан 59 265 -> 55 257, синяя 223 902 ->
219 753, работа кадра 464 796 -> 456 780.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:33:31 +03:00
snark13 52bcc65a62 Особенность: убитый за правым краем страж не виден ни в одной комнате
Вопрос пользователя после боя в комнате 15: Кид вытеснил стража вправо
(из-за края торчал только меч), убил — труп не появился ни в 15, ни в
соседней справа.

Это не наш баг, а сложение трёх механизмов оригинала: физика стража
работает только в полосе x 44..211, поэтому комнату он не менял;
мёртвый за Кидом не идёт (follow_guard требует alive < 0), и leave_guard
сохраняет его в прежнюю комнату; а из чужой комнаты страж не рисуется
вовсе — при Guard.room != drawn_room оригинал гасит слот (seg000:422).

Труп остаётся приписан комнате 15 с guards_x за правым краем: при
возврате восстанавливается там же, то есть вне видимого поля.

Живьём в SDLPoP сценарий не воспроизводился — вывод из чтения кода, о чём
в записи сказано прямо.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:23:41 +03:00
snark13 41deb69013 Реестр: P15 закрыт замерами обеих позиций
Лёгкая: 628 542 -> 464 796 (-163 746), персонажи не рисуются вовсе.
Тяжёлая: 758 358 -> 617 487 (-140 871), рисуется только Кид — он
действительно стоит под пламенем, а страж нет.  Пятирастровые кадры в
тяжёлой позиции исчезли (было 27 %).

Итог восьми позиций: 801 768 -> 464 796 в лёгкой (-42 %).  До цели
430 000 осталось 35 000 в лёгкой и 187 000 в тяжёлой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:09:47 +03:00
snark13 767d6f78a4 P15: метка «фон трогали» стала точной — минус 167 880 тактов на кадре
Две правки, обе про ложные срабатывания пропуска отрисовки персонажа.

1. МЕТКА: вместо «маска колонок по 32 px на ТРИ ряда по 63 px» теперь на
   каждую колонку хранится диапазон затронутых y (ymin/ymax, 40 байт на обе
   страницы).  Прежняя гранулярность склеивала касания внутри ряда: пламя
   факела занимает y 33..50, клинок стоящего стража — y 59..65, между ними
   девять пикселей зазора, а метка считала слот задетым.

2. ПРОВЕРКА: cd_quiet сверяет с меткой спрайт и накладной (клинок, брызги)
   ДВУМЯ ОТДЕЛЬНЫМИ прямоугольниками, а не объединённым bbox.  Объединение
   включает пустой угол между ними, и он ловил касания, которых нет: спрайт
   стража лежит в колонке 8, клинок уходит в колонку 7 на y 59..65, пламя
   метит колонку 7 на y 33..50 — прямоугольник «спрайт + клинок»
   (x 241..284, y 46..84) цеплял метку углом.

Без второй правки первая почти ничего не дала (632 676 против 628 542 до
неё): объединённый bbox продолжал ловить ложное пересечение.

Замер 11/15:

  фаза      до P15    после
  синяя    259 050   223 902   (heal тоже перестал платить)
  зелёная  181 494   181 494
  циан     194 262    59 406
  работа   632 676   464 796

Проверено в MAME: в статике картинка чистая, в динамике (пробежка, бой,
переход в соседнюю комнату) хвостов и просвечивания нет.  Хост-тесты
зелёные.

Заодно найден и исправлен собственный баг первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0, и после
pop_cd_clear(0) метка страницы 1 переставала расти.  Плюс pop_cd_init:
пустая колонка обозначается ymin = 255, а нули от crt0 читались бы как
«затронута строка 0».

У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast,
шесть буферов по блокам), и SDLPoP (set_redraw_fore) метят целыми тайлами,
но им это не мешает — персонаж у них рисуется каждый кадр безусловно.
Пропуск неизменившегося персонажа — наша добавка, поэтому и точность метки
нужна выше оригинальной.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:04:46 +03:00
snark13 17c41b32de P15 переписан: перекрытия нет, виновата грубость метки
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.

Проверка по памяти машины подтвердила и уточнила:

  страж, спрайт   x 257..284  y 18..56
  страж, клинок   x 241..261  y 31..37
  пламя факела    x 232..247  y  5..22

Ошибок было две.  Первая: координаты пламени я взял по предположению
«факел в колонке 7», а он в колонке 6 (пламя рисуется в ячейке правого
соседа).  Вторая, содержательная: ФИЗИЧЕСКОГО ПЕРЕКРЫТИЯ НЕТ ВООБЩЕ — по x
клинок и пламя пересекаются, но по y между ними девять пикселей зазора.

Настоящая причина: pop_cd_touch хранит метку как маску КОЛОНОК по 32 px на
ТРИ ряда по 63 px (cd_row_of).  Пламя (y 5..22) и клинок (y 31..37)
попадают в один ряд 0 и одну колонку 7 — cd_quiet считает слот задетым.
148 302 такта, 23 % кадра, за ложную тревогу.

Решение стало проще и точнее: хранить на колонку диапазон y вместо номера
ряда (10 x 2 байта x 2 страницы = 40 байт).  Расчётом проверено, что это
спасает стража и НЕ спасает Кида в тяжёлой позиции — там перекрытие
настоящее, и он честно перерисовывается.  Вариант с 8-пиксельными полосами
тоже работает, 16-пиксельные уже нет.

Прежние предложения (частичная перерисовка по пересечению, обрезка фона под
персонажем) записаны как НЕ НУЖНЫЕ: они решали задачу, которой нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:40:30 +03:00
snark13 a65da96960 CHAR-PARTIAL-REDRAW: обязательная задача о неподвижном персонаже
Постановка пользователя: проверять, нужна ли отрисовка стража, когда он
не двигается.  Если движется — лишние ~150 000 тактов приемлемы: в
оригинале во время боя число физических кадров на логический тоже растёт
на единицу.

Замер чтением pop_cd из памяти машины показал, насколько цена
несоразмерна поводу:

  страж       x 257..284, y 18..56   28 x 39
  пламя (0,7) x 264..279, y  5..22   16 x 18
  пересечение x 264..279, y 18..22   16 x 5

То есть пламя задевает страже только макушку — 80 пикселей, — а
перерисовывается он целиком за 148 302 такта (85 524 спрайт с клинком и
снимком + 62 778 fore-проход), это 23 % работы кадра.  Пересечение при
этом настоящее: дело не в грубости маски меток, проверено числами.

В задаче записаны два варианта: A — частичная перерисовка только
пересечения (безопаснее, укладывается в контракт pop_cd), B — не рисовать
фон там, где он всё равно перекрыт неподвижным персонажем (дешевле, но
обрезанное пламя попадёт в ОЗУ-копию и heal вернёт дыру, когда персонаж
сдвинется).

Заодно уточнено, чем НЕ является P13 (вопрос пользователя): это не
перерисовка комнаты заново каждый кадр — такой вариант стоил бы порядка
3 000 000 тактов, семь растровых кадров, и оригинал так тоже не делает.
Разница в цене ПОСЕЩЕНИЯ тайла: у нас fore_tile сразу блитит, у оригинала
add_*table только кладёт запись, а рисует один draw_table в конце.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:35:54 +03:00
snark13 d3049693b8 Реестр: сводка и статусы обновлены; замер тяжёлой позиции
Пользователь передвинул Кида на один осторожный шаг вправо (x = 106
вместо 99, колонка та же) — его спрайт начал пересекаться с тайлом (0,3),
где одновременно чомпер и пламя факела.

  фаза      лёгкая    тяжёлая
  синяя    259 500    257 520
  зелёная  181 068    180 870
  циан     187 761    319 842   (+132 081)
  работа   628 542    758 358

Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %).  Это уже не
«стабильно медленно», а рывки.

Куда ушли 132 тысячи: pop_char_draw(KID) 204 -> 54 738 и fore-проход
Кида 4 356 -> 93 486.  То есть Кид из «пропущен» превращается в
полноценного персонажа за ~144 000 — столько же, сколько страж.

Отсюда новая позиция P14: fore-проход персонажа, 62 778 у стража и
~89 000 у Кида, вместе около 152 000 = 20 % работы кадра.  Это самая
дорогая единичная статья.  У Кида он дороже потому, что в его футпринте
лежит чомпер со своим передним слоем.

P3 переведён в «частично сбылось»: выигрыш держится только пока персонаж
не подошёл к анимированному тайлу, а в игре он подходит постоянно.

Итог семи закрытых позиций: 801 768 -> 628 542, то есть -22 %.  До цели
430 000 остаётся снять 199 000 в лёгкой позиции и 328 000 в тяжёлой, а
всё оставшееся в реестре даёт порядка 100 000.  Арифметика не сходится —
в реестр записаны три возможных решения (P13, осознанное расхождение с
оригиналом, принять 4 растра), выбор за пользователем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:30:04 +03:00
snark13 6faf81016a Замер цианной фазы: крупного лишнего в отрисовке персонажа нет
Циан 188 004 не двигался ни от P1, ни от P5, ни от P2b — разложил его
зондами.

Хорошая новость: Кид УЖЕ пропускается (204 такта на pop_char_draw), то
есть надежда P3 сбылась после P1 — метка от чомпера до него больше не
дотягивается.  Страж же перерисовывается каждый кадр честно: пламя
правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально
накрывает ему голову (пламя занимает y 5..22, страж 12..62).

Отрисовка стража — 148 302:

  pop_char_fore (2 трамплина в банк 2 + обход тайлов)  62 778   42 %
  клинок (sword_draw + overlay_add + clip_add)         27 522   19 %
  блит спрайта + clip_char_right                       20 982   14 %
  загрузка кадра и геометрия                           12 696    9 %
  pop_clip_char_top (трамплин банк 4 -> банк 3)         8 658    6 %
  снимок прямоугольника + cd_clip_add                   7 890    5 %
  gfx_w0_unmap + cd_sig_make                            4 968    3 %
  вход + cd_heal                                        2 946    2 %

Единственная явно лишняя статья — трамплин clip_char_top, и снять его
непросто: функции нужны get_tile и таблицы деления из банка 3, перенос в
резидент вернёт тот же трамплин внутрь.  Остальное — работа, которую
персонаж действительно делает.

Зонды переставлены с уже закрытых замеров (loose_tick, физика) внутрь
pop_cdraw; оснастка снимается позицией P12, когда оптимизация закончится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:53:49 +03:00
snark13 068e21b56a Реестр: P2b закрыт; иерархия референсов и трамплины в цикле
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:37:34 +03:00
snark13 f8493a4c04 P2b: луч видимости стража 36 786 -> 13 002 (-65 %)
Замер отделил луч от pop_frame_timers: таймеры со всеми тремя
спецсобытиями уровней стоят 1 962, луч — 36 786, то есть 5,6 % работы
кадра на девять чтений байта.

Причина оказалась НЕ в алгоритме.  Сверка трёх референсов:

  SDLPoP (seg003:688) — идёт по x с шагом 14 и на каждом шаге переводит x
    в колонку делением.  Причём сам SDLPoP признаёт в комментарии, что
    «DOS PoP does this: tile_div_tbl[xpos]» — то есть оригинал брал
    таблицу, а порт заменил её на / и %, потому что на 32 битах так проще.
  Apple II (MISC.S CHECKALERT) — тот же алгоритм байт в байт, но перевод
    x -> блок через таблицу BlockTable[x].  Ровно то, что у нас уже было
    сделано (POP_TILE_DIV, 2026-08-10).
  mininim — другая архитектура (тайловые позиции, своя механика), для
    сравнения реализации не годится.

То есть алгоритмически мы уже были на уровне Apple II, а платили за
другое: pop_tile_at объявлен __banked, луч живёт в guards.c (банк 1), и
на КАЖДУЮ колонку шёл трамплин банк 1 -> банк 3.  На сцене 11/15 (Кид в
колонке 2, страж в 8) это девять трамплинов за кадр.

Сделано:

  1. луч переведён на КОЛОНКИ вместо x-координат.  Это эквивалентно:
     начальные x — ровно центры тайлов персонажей, а обратный перевод даёт
     ту же колонку (floor((58 + col*14 - 58)/14) == col).  Ушли 16-битный
     шаг, 16-битное сравнение и индексация таблицы на каждой итерации;
  2. тайлы отрезка забираются ОДНИМ банковым вызовом (pop_row_tiles)
     вместо девяти;
  3. внутри pop_row_tiles — быстрый путь для отрезка целиком внутри
     комнаты: get_tile при ряде 0..2 и колонке 0..9 сводится ровно к
     g_fg[row*10+col] & 0x1F, идём указателем;
  4. буфер тайлов — file-scope, а не локальный массив (иначе каждое
     чтение это -n(ix)).

Замер по шагам: 36 786 -> 24 048 (колонки + один вызов) -> 13 002
(быстрый путь + буфер).  Синяя фаза 283 215 -> 259 500, работа кадра
654 990 -> 628 542, то есть -26 448 при ожидании -30 000.

Кэш-гейт «пересчитывать только при смене позиции» НЕ понадобился:
расхождения с оригиналом нет, луч считается каждый кадр, как и должен.

Поведение проверено в MAME: страж в боевой стойке, но не идёт — между ним
и Кидом чомпер, то есть can_guard_see_kid = 1 («видит, но не пойдёт»).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:37:06 +03:00
snark13 da17a48576 Реестр: P2a закрыт, следующий P2b
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:09:30 +03:00
snark13 2bdaf0f4cd P2a: coll_scan переведён на 8 бит и снят с IX — минус 3 486 в коллизиях
Разбор pop_phys_tick (61 266 тактов на НЕПОДВИЖНОМ Киде) зондами по
звеньям kid_phys:

  check_collisions      33 846   55 %
  хвост (spike/spiked/chomped/knock/leave/save)  16 188   26 %
  check_press            4 140
  check_action           2 622
  loadkid_and_opp        2 148
  determine_col          1 182
  fall_accel+fall_speed    582
  bump_into_opponent       198

Внутри check_collisions: три coll_row (сканирование рядов) — 23 256,
подготовка окна 3 240, set_char_collision 1 788, обход пересечения 5 562.

Сгенерированный asm coll_scan показал 322 такта Z80 на ПУСТУЮ колонку
(с wait-state'ами 773 — ровно замеренные 750), из них 137 (43 %) —
обращения через IX-фрейм, и четыре 16-битные операции на колонку там,
где от колонки зависит один операнд.

Сделано:

  1. вся арифметика цикла в 8 битах.  scan_left = x_bump[col+5] + TILE_MIDX
     при колонках окна -2..11 лежит в [37, 233], wall_dl в [-1, 10],
     wall_dr в [0, 13] — суммы в [36, 246], переполниться не могут.
     Границы персонажа приводятся к 8 битам с клипом, и клип точен: порог
     ниже 37 означает «условие не выполнится никогда», выше 233 — «всегда».
  2. dst снят с IX-фрейма в file-scope (scan_dst).

ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ, не повторять: предпосчёт таблиц порогов по типу
стены (thr_l[6]/thr_r[6] на кадр) сделал ХУЖЕ — check_collisions
33 846 -> 36 570, синяя фаза +10 269.  Колонок в окне четыре-пять, а типов
стен пять: кэша получилось больше, чем потребления.

Проверено на кодогенерации: register на параметре-указателе SDCC 4.5 z80
проигнорировал (asm байт в байт), а file-scope дал 607 -> 454 такта.

Итог: check_collisions 33 846 -> 30 360 (-10 %), работа кадра
657 882 -> 654 990.  Крупной статьи в физике нет: остаток размазан по
десятку честных проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:06:43 +03:00
snark13 de68eb5cec P1: чомпер перерисовывался неизменной позой — минус 110 802 такта
Позиция заводилась с НЕПОЛНЫМ диагнозом.  Я приписал 190 260 тактов
пометке от факела (пламя лежит в ячейке правого соседа, то есть поверх
чомпера, и запекается каждый кадр).  Правка по этому диагнозу не дала
ничего: 769 002 против 768 684.

Зонд pop_dbg_kind показал факт: все 312 перерисовок прогона — вид
POP_RD_CHOMP, полная, и ни одной от факела.  Собственная пометка чомпера
просто перебивала пометку соседа.

Настоящая причина нашлась сверкой с animate_chomper (seg007:0448).
Оригинал заканчивает её так:

    if ((curr_modifier & 0x7F) < 6) redraw_at_trob();

то есть перерисовывает чомпер только пока фаза меньше 6 — пять кадров из
пятнадцати.  Это не оптимизация оригинала, а следствие таблицы поз:
chomper_fram1 = {3,2,0,1,4,3,3}, и с фазы 5 до конца круга поза одна и та
же.  Мы метили тайл каждый кадр, пока trob жив, а живёт он всё время, пока
Кид в том же ряду — то есть платили полный draw_tile плюс heal 32x64 за
неизменную картинку в двух третях кадров.

Сделано:

  1. пометка только при фазе < 6; на фазе 5 — обе страницы дабл-буфера
     (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу
     пометка догоняет в кадре фазы 6, где поза та же — CHOMP_FRAM1[6] == 3);
  2. новый вид POP_RD_CHOMP_ANIM -> pop_chomp_anim_draw: три блита графики
     чомпера поверх свежего пламени, без heal и без остальных слоёв — порт
     ветки redraw_frames_anim (seg008:0211), где оригинал делает ровно
     draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim и
     никакого wipe;
  3. приоритет полной перерисовки над anim в pop_set_redraw: у оригинала
     это два независимых счётчика и full побеждает, а у нас вид один на
     тайл, и без проверки исход решал бы порядок trob'ов в списке.

Обе половины работают — замер даёт 40 % полных перерисовок и 60 % лёгких.
Работа 768 684 -> 657 882 (медиана), зелёная 294 510 -> 183 420.  В 40 %
кадров цена прежняя: там поза реально меняется, это честная работа.

Циан не сдвинулся ни на такт, то есть надежда P3 (Кид перестанет будиться
каждый кадр) пока не оправдалась — метки продолжают его будить.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:30:01 +03:00
snark13 f81b30eb68 Замер P2 и P6: главные статьи — физика двух Char и луч видимости
Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs
(89 784) — на три.

Гипотеза, с которой я входил в замер, ОТВЕРГНУТА.  Я ждал, что дорого
обходятся банковые трамплины на спецсобытиях уровней — по аналогии с
pop_clip_char_top, где трамплин ради одной проверки стоит 8 892.  На деле
три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они
гейтятся внутри и на уровне 11 выходят сразу.

Настоящие статьи синей:

  физика двух Char        105 246  (61 266 Кид + 43 980 страж)
  heal двух Char           67 734
  луч видимости стража  до 37 032  (вместе с frame_timers)
  pop_ctrl_tick            18 648
  логика стража            19 932

Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр
позы не меняется.  Цена измерена, причина нет — это отдельная позиция P2a.
Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене
позиции/комнаты — позиция P2b, ждём −30 000.  heal отдельной правки не
требует, он уйдёт вместе с P1/P3.

process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход
самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ
trob, хотя комната одна), два факела ~36 000.  Цена одного pop_pot_b
измерена отдельно: 17 346, и это единственная группа в распределении —
значит в кадре его зовут только факелы.  Пиксели пламени 16x18 — 1 716,
то есть 10 % цены.

Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма
ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной
(P1).  Цель достижима, но без запаса.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:05:58 +03:00
snark13 6d7c1c8b6b P5: гейты холостого хода в pop_loose_tick — минус 33 840 тактов на кадре
Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где
не анимируется ни одна плита и не летит ни один кусок.  Раскладка зондами
m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск
куска над головой Кида 5 868 (там ещё и банковый трамплин).

Два гейта:

  loose_any (статик pop_map.c) — «идёт ли анимация плит».  Ставят пять мест
  записи ненулевой фазы: make_loose_fall, ветка потолка в check_press,
  do_knock для обоих рядов и восстановление фазы из room_modif при входе в
  комнату.  Снимает его сам цикл, по факту прохода, в котором не осталось
  ни одной живой фазы.

  pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска»
  (active или дочистка clean).  Ставит mob_alloc, снимает обход по факту
  пустой таблицы.  В резиденте, а не в pop_room.c, потому что читает его
  pop_map из банка 3, а писучие статики банкового модуля наружу не видны.

Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у
лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя).
Асимметрия намеренная — ложная единица стоит одного холостого прохода,
ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с
КАЖДОЙ записью, а снятие только по факту пустого прохода.

Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760.  На кадре работа
801 768 -> 767 928.  Ожидание по реестру было -28 000.

Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода
(фаза восстановлена входом в комнату) — единственное, которое не прогонял
ни один тест, и дающее самый тихий отказ.  Мутационная проверка: со снятым
взводом тест падает (фаза 3 вместо 4).

Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2
(дефолт), roomtest стартует в 11/15 с Кидом в (0,2).  kid_init ставит
x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё
физика относила Кида в тайл чомпера, и он погибал на старте (найдено
пользователем).  Сдвиг внутрь на 2: колонку определяет весовая точка кадра,
поэтому число снято замером, а не выведено геометрией.

План работ между сессиями — docs/perf_registry.md §4: очередь позиций со
статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:49:19 +03:00
snark13 fd570c7eb8 Замер сцены 11/15 и единый реестр оптимизаций
Новая целевая сцена: уровень 11 комната 15 — два факела, чомпер, страж.
В отличие от 13/23 (разовый пик на каскаде плит) здесь дорога САМА
статика: Кид и страж стоят, а кадр стоит 801 768 тактов = 1,86
растрового кадра, период 4 растра во всех 866 интервалах прогона.

Фазы: синяя 293 238 (heal 141 048 + логика 152 190), зелёная 320 916
(loose_tick 28 872 + process_trobs 92 448 + redraw_needed 190 260),
циан 187 758.  Впечатление «циан ~150 % кадра» не подтвердилось: за 867
кадров разброс циана 174 такта, это 0,44 растра.

Главная находка — 190 260 тактов на ОДИН тайл (pop_dbg_rdmax_tot = 1).
Пламя факела запекается в ячейке правого соседа, то есть поверх чомпера,
и process_trobs метит соседа (порт set_redraw_anim_right).  Оригинал на
такую пометку рисует ТОЛЬКО слой anim, мы же отвечаем heal 32x64 плюс
полный draw_tile — со всеми слоями, которых пламя не касалось.

Заодно разложена цена одного блита фона (брейкпоинты на резидентных
адресах внутри pop_blit_b, temp0 на входе, 1603 блита): фиксированная
накладная 6 126 тактов на ЛЮБОЙ блит — пролог с IX-фреймом 810,
atlas_image 672, w0_map с чтением шапки 2 400, cd_touch 2 069, unmap 175.
У самого дешёвого блита это 59 % цены, у пламени 16x18 пиксели тянут
лишь 12 %.  Причины ровно те, на которые указал пользователь:
16-битные аргументы там, где хватает 8 бит, и адресация через IX.

perf_registry.md сводит в один отсортированный список всё отложенное из
perf_green_phase (G1-G9), perf_cyan_phase (C1-C7), perf_backlog (1-7),
HEAL-WIDTH и сегодняшние находки — с пометкой замер/модель/гипотеза.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:21:02 +03:00
snark13 09f32ce834 Уровни 10-12 прошли предварительный тест; HEAL-WIDTH связан с G8
Уровень 11 комната 14 проверена пользователем визуально после варианта B —
порядок падающего куска, соседней плиты и Кида корректен.  На 10 и 12
багов не найдено.

HEAL-WIDTH и G8 сведены как две половины одной темы: G8 про ширину ЗАПЕЧКИ
соседнего тайла (60 вместо 28 нужных, плюс draw_tile соседа дважды на
пометку), HEAL-WIDTH про ширину HEAL'ов (64 вместо фактических 58/57).
Оговорка из G8 перенесена: 60 = 32 свой тайл + 28 собственный свес, для
запечки самого тайла это минимум, сужать можно только пометку СОСЕДА.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mob-order-B-done
2026-08-19 00:00:23 +03:00
snark13 4db60c750f Вариант B: кусок с завёрнутым рядом рисуется под всем (корзина 30)
Порт правила оригинала, разобранного в 272cf8f.  y_to_row_mod4 даёт −1 и
для куска выше потолка, и для ушедшего ниже комнаты; get_tilepos_nominus
сводит оба в тайл 30, а объекты тайла 30 рисуются в redraw_needed_tiles
ПЕРВЫМИ, до всего обхода тайлов.

Что сделано:
 - defer = 0 для таких кусков: они под всем, включая Кида.  Раньше
   сравнение рядов читало −1 как «обходится последним» = «поверх всего»;
 - оверлею отдаётся ориентир 3 («раньше любого ряда 2,1,0») вместо сырого
   −1 — гейт other_overlay_tile перестал отбрасывать возврат соседа, из-за
   чего тело плиты не возвращалось и оставался только её торец из
   переднего слоя;
 - в набор перекрываемых тайлов добавлена СВОЯ клетка (только для этого
   случая: у куска в обычном ряду объект вливается в midtable после частей
   своего тайла, и перерисовывать её нельзя).

Отладочная обвязка разбора (журнал решений оверлея, маска перекрывающих
тайлов) снята; счётчик перерисовок за кадр в pop_redraw_needed оставлен —
он дешёвый и пригодится для HEAL-WIDTH.

ЗАМЕР 13/23, 3032 кадра, против тега mob-order-B-start:
  работа  888 984 -> 913 848  (+24 864)
  синяя   159 804 -> 159 810
  зелёная 427 242 -> 440 418  (+13 176)
  циан    378 864 -> 393 000  (+14 136)
Период кадра не изменился: 4 растра в 23 кадрах, 5 в двух.

Зелёная вышла за растровый кадр (440 418 против 430 000).  Детализация:
pop_loose_tick 185 826, из них pop_loose_mob_tick 168 180; тробы +
redraw_needed 337 800.  Разбор и план возврата тактов — HEAL-WIDTH.

Визуальная проверка комнаты 14 за пользователем: поймать кадр с куском
снимками мне не удалось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:10:41 +03:00
snark13 272cf8f195 Разбор порядка отрисовки падающей плиты: найден корень, выбран вариант B
СИМПТОМ (пользователь, ур.11 к.14).  Кусок, отвалившийся от плиты нижнего
ряда, рисуется ПОВЕРХ соседней трясущейся плиты; при этом передний торец
соседа лежит поверх куска — порядок противоречив в разных частях
перекрытия.

КОРЕНЬ.  draw_mob (seg007:13E5) считает ряд объекта как
y_to_row_mod4(y) = (y+60)/63 % 4 - 1.  Из-за % 4 ряд 3 (кусок ушёл ниже
комнаты) и ряд −1 (кусок у потолка) дают ОДНО значение −1.  Оригинал
прогоняет его через get_tilepos -> get_tilepos_nominus и получает тайл 30,
а объекты с тайлом 30 рисуются в redraw_needed_tiles ПЕРВЫМИ, до всего
обхода тайлов.  Мы же передаём сырой −1 в мид-оверлей как «тайл объекта», и
гейт `row > pop_bg_obj_row` читает его как «объект в последнем ряду обхода»,
то есть «объект поверх всего», и отбрасывает возврат соседа.  Один и тот же
−1 у нас значит «сверху», у оригинала — «снизу».

Торец при этом виден потому, что приходит из ДРУГОГО слоя: draw_loose кладёт
loose_fram_bottom в backtable и foretable, минуя ptr_add_table, — тело плиты
обязан вернуть мид-оверлей, а его и выключает гейт.

ЧТО ПРОВЕРЕНО ЗАМЕРОМ (журнал решений в pop_dbg_ovl/pop_dbg_pass, зонды
ВРЕМЕННЫЕ и будут сняты):
 - на застывшем кадре: слот draw_y=194, r=−1, rt=2, оверлей позван, внутри
   отбрасывается гейтом;
 - пропуск оверлея на последнем кадре полёта ЗАКОНЕН: габарит куска уже
   ниже габарита тайла, перекрывать нечего;
 - в 13/23 кусок перекрывают 2-4 тайла (накопленно за полёт), включая СВОЮ
   клетку, — то есть «сосед справа» покрытие не исчерпывает;
 - перерисовок тайлов за кадр в 13/23: максимум 6, в покое 0.

ТУПИКИ, чтобы не ходить второй раз.  Клип объекта тут ни при чём:
add_mob_to_objtable ставит clip.right = 40, но клип применяется только при
chtab_flip_clip[chtab_id], а для chtab_6_environment там 0 — поле
игнорируется и в оригинале.  Пункт MOB-CLIP-RIGHT закрывается как
несуществующий.  Добавлять торец плиты в мид-оверлей тоже не надо: в
foretable он уже кладётся из fore_tile.

ВЫБРАН ВАРИАНТ B: классифицировать ряд объекта (вне 0..2 = корзина 30),
отдавать оверлею ориентир «раньше любого тайла» и расширить набор
перекрываемых тайлов на СВОЮ клетку.  Переносить проход отрисовки не нужно —
heal при этом не участвует, работа та же (оверлей = два блита в окне клипа),
разница с узким вариантом A всего один-два оверлея на кусок.

Заодно записана ОБЯЗАТЕЛЬНАЯ задача HEAL-WIDTH: ширины точечных heal'ов
взяты по клеткам (64), а фактический след плиты — 58/57 (замерено по
атласам: верх 32, правая грань 26 в подземелье и 25 во дворце).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mob-order-B-start
2026-08-18 22:51:00 +03:00
snark13 cdac5746ac Мид-оверлей не рисовал передний торец плиты (draw_loose из draw_tile2)
Правый конец падающего куска лежал поверх соседней плиты.  У оригинала
мид-оверлей — draw_tile2(), и его последний вызов draw_loose(0) рисует
передний торец плиты; у loose bottom_id = 0, поэтому больше его не рисует
никто, и в overlay_mid_tile торца не было вовсе.

Заодно снят вопрос про клип объекта: add_mob_to_objtable ставит куску
clip.right = 40, но клип применяется только при chtab_flip_clip[chtab_id],
а для chtab_6_environment там 0 — поле игнорируется и в оригинале.  Значит
«клипа нет» у нас верно, MOB-CLIP-RIGHT закрывается как несуществующий.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:16:59 +03:00
snark13 ed5615a95a Плиты нижнего ряда пропадали без кадров падения: draw_mob рисует и соседей
Уровень 11 комната 14: плита ряда 2 уходит в комнату снизу на первом же
move_loose (спавн y=191 при границе ряда 188), а у нас кусок в чужой комнате
не рисовался вовсе — плита исчезала мгновенно.  Оригинал (seg007:13E5)
рисует его ещё три кадра, выглядывающим из нижней кромки (+192), и
симметрично из комнаты сверху (−189).

У куска появилась экранная координата draw_y; по ней идут отрисовка, heal,
порядок относительно Кида, пометки соседа и оверлей, отбор в проходе — по
ней же, а не по комнате.  Ссылки вверх/вниз кэшируются.

Цена (13/23): зелёная 419 562 -> 427 242, работа 880 272 -> 888 984.
Первый вариант стоил втрое дороже (445 248) из-за безусловной пометки по
прошлой нарисованной позиции — у куска из соседней комнаты она в 192
пикселях, и объединение растягивалось на весь экран; гейт вернул 18 000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:55:39 +03:00
snark13 0832445614 Уровень 9 прошёл предварительный тест; замер тактов 13/23 без регресса
Уровень 9 (зелье инверсии) — багов не найдено.

Регресс 13/23, 2701 кадр: работа 880 272, синяя 159 822, зелёная 419 562,
циан 380 202 — против 880 170 / 159 774 / 419 520 / 380 244 у 3bcaf51.
Разброс ±100 тактов на 880 000 (0,01 %), циан даже в минус.  Период совпал
кадр в кадр: 4 растра в 24 кадрах, 5 в одном.  Host-тесты 5106 проверок
без расхождений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:49:37 +03:00
snark13 a737412154 Уровень 8 прошёл предварительный тест
Найдено и закрыто по дороге: GUARD-RESPAWN-COL0 (c40ae3f) — страж при
возврате в комнату телепортировался в колонку 0 и падал насмерть;
SEAM-FIGHT-FLICKER (a498255) — бой у шва перерисовывал комнату
туда-обратно, поведение сверено с SDLPoP покадрово.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:25:08 +03:00
snark13 3f643532c9 Трасса kidobj для сверки с SDLPoP + разбор боя у шва
Инструмент: pop_dbg_kidobj печатает то же, что вывод, добавленный
пользователем в SDLPoP add_kid_to_objtable — tilepos/frame/act/col/cols/rows
(+ наша комната).  Считает по set_char_collision и set_objtile_at_char.
Выключен по умолчанию (DBG_KIDOBJ 0), включается одним define; вывод
забирает брейкпоинт MAME на резидентном pop_dbg_trap.

Сверка подтвердила фикс a498255: окно перехода совпало кадр в кадр, за бой
на уровне 1 комната сменилась один раз, на уровне 8 у шва 24/18 — четыре
раза на 387 кадров боя, и все четыре на РАЗРЕШЁННЫХ кадрах (170/164/170/165).
Смен на запрещённых кадрах во всей трассе нет.

Разбор целиком — BUGS_CLOSED.md#seam-fight-flicker, включая невыясненное
расхождение поля cols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:07:01 +03:00
snark13 a498255471 Бой у шва: смену комнаты запрещают ещё и кадры меча (leave_room, seg002:0490)
Пользователь: при бое на шве (Кид в 18, страж в 24) комната перерисовывается
то одна, то другая — драться невозможно.

leave_room запрещает уход не только на развороте, подъёме-с-зацепа и
вставании из приседа, но и на всей боевой анимации: кадры 150..162 и
166..168.  У нас были только первые три условия, поэтому отступающий и
наступающий Кид пересекал границу почти каждый кадр.

Оригинал пускает смену комнаты только на «легальном» кадре вроде 170.
Известный побочный эффект — Trick 35 «retreat without leaving the room»;
в SDLPoP его чинит FIX_RETREAT_WITHOUT_LEAVING_ROOM, по умолчанию
выключенный, так что портируем оригинал.

Сторона стража проверена отдельно и расхождений не дала: play_guard_frame
(seg000:0F48) ухода из комнаты не содержит вовсе, страж меняет комнату
только через follow_guard (есть, pop_guard_follow) и check_guard_fallout
(есть, pop_guard_fallout).

Банк 3 +10 Б, резидент без изменений.  Host-тесты 5106 проверок чисто.
Сам бой у шва не воспроизводился — проверка за пользователем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:35:12 +03:00
snark13 c40ae3f8f5 Страж возвращался в комнату не туда: колонку несёт guards_x, а не тайл
Уровень 8 комната 24: после первого же выхода Кида страж телепортировался
в колонку 0, а ряд 0 там пустой в колонках 1..3 — страж падал с ряда 0 на
ряд 2 и разбивался.

leave_guard (seg002:02F5) кладёт в guards_tile get_tilepos(0, row), то есть
обнуляет колонку НАМЕРЕННО: позицию по горизонтали несёт guards_x, туда же
она и пишется.  enter_guard берёт x оттуда всегда.  Мы же считали x из
tile % 10 (то есть из нуля), а запомненную брали только у трупа.

Заодно портирован pos_guards (seg003:0913): при загрузке уровня guards_x
пересчитывается из колонки тайла, а файловое значение выбрасывается — в
комнате 24 уровня 8 там 255.

Проверено в MAME: три входа подряд дают одну и ту же позицию (x 156, ряд 0),
страж стоит на полу.  Host-тесты 5106 проверок без расхождений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:20:40 +03:00
snark13 0077ef3850 Задача PERF-SWEEP: поиск узких мест по всей игре, а не в одной сцене
Постановка пользователя: ручной проход уровня с логом фаз + комната/тайл,
дальше оптимизация конкретной комнаты.  Записано вместе с блокером —
брейкпоинтами это делать нельзя (эмуляция падает в проценты от реального
времени, играть невозможно); варианты: тап на порт бордюра в Lua-мосте
(не привязан к адресам кода) или самозамер программой в растрах (работает
и на железе).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:59:11 +03:00
snark13 ed7e63a4fa Замер тактов 13/23 после фиксов уровней 3-7: регресса нет
2435 кадров сцены каскада.  Максимумы по секциям: работа 880 170,
синяя 159 774, зелёная 419 520, циан 380 244 — против 878 550 / 158 880 /
419 526 / 379 488 у ec1f384.  Все четыре в пределах шума прогона, зелёная
совпала до 6 тактов.  Распределение периода совпало кадр в кадр: 4 растра
в 24 кадрах, 5 в одном, остальные 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:50:26 +03:00
snark13 a6666489c0 Уровень 7 прошёл предварительный тест
Найдено и закрыто по дороге: SPIKE-BAKED (980d48c), DIED-ON-BUTTON
(6feab5d).  Регресс после них: сцена 13/23 (каскад плит) отрабатывает
штатно, host-тесты 5106 проверок без расхождений — включая 1730 трасс
физики, которых касалось снятие раннего выхода для трупа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:36:51 +03:00
snark13 3bcaf51221 NEXT_SESSION: DIED-ON-BUTTON закрыт
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:28:19 +03:00
snark13 6feab5d2f2 Смерть на кнопке ломает её насовсем (died_on_button, seg007:776)
Уровень 7 начинается падением; разбившись на кнопке открытия решётки
(комната 3, тайл 2,1), Кид в оригинале открывает решётку НАСОВСЕМ, а у нас
она закрывалась обратно.  Чинить пришлось три места.

1. pop_phys_tick выходил по pop_kid_dead, то есть check_press до трупа не
   доходил вовсе.  У оригинала play_kid_frame гейтится только Char.room
   != 0.  Кнопка получала одно нажатие — в кадре смерти, потому что флаг
   ставит land() уже внутри цепочки.  «Труп не шевелится» держит внутренний
   выход в kid_phys, он остался.

2. Портирован died_on_button: открывалка → пол + связь дёргается типом
   «щебень» (открыть насовсем), прочие кнопки → TILE_STUCK.  Ветки по
   Char.alive в check_press не было вовсе.

3. Рестарт уровня восстанавливал только foretable, а died_on_button
   оставляет таймер связи нажатым (trob кнопки умирает сразу — тайл уже
   пол).  Остаток переживал респавн, и кнопка рисовалась нажатой с первого
   кадра.  pop_level_reset_tiles теперь возвращает и LINKMAP.

Проверено в MAME по всему циклу: смерть → FF (открыта навсегда), респавн →
кнопка цела и не нажата, второе падение → снова ломается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:27:36 +03:00
snark13 b49ba15666 DIED-ON-BUTTON: найдена вторая половина — физика трупа выключена целиком
Кид гибнет на кнопке открытия решётки (уровень 7, комната 3, тайл 2,1) —
в оригинале решётка открыта насовсем, у нас отжимается.  Мало того, что
died_on_button (seg007:776) не портирован: pop_phys_tick целиком выходит
по pop_kid_dead, так что check_press до трупа не доходит вовсе.  У
оригинала play_kid_frame гейтится только Char.room != 0.

Фикса пока нет — запись в BUGS_OPEN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:11:33 +03:00
snark13 980d48c168 Пики оставались навсегда: запечка фона консервировала выдвинутый кадр
Уровень 7, комната 19.  Кнопка в (0,5) при нажатии зовёт pop_floor_bake,
а тот перерисовывает и правого соседа — пики (0,6) — в банке, который
пишет в ОЗУ-копию.  Кадр брался живой, и выдвинутая пика оставалась в
фоне: дальше heal возвращал её каждый кадр.  В (0,7) чисто, потому что
окно клипа запечки кончается на x=219, а колонка 7 начинается с 224.

Флаг pop_t_bake_rest («в фон кладём покой») для этого и был, но читал его
только pop_loose_frame.  Кадр пик считался по месту в ЧЕТЫРЁХ слоях.
Добавлен pop_spike_frame — порт get_spike_frame (SDLPoP seg008:08A0),
которого у нас не было, — и все четыре слоя переведены на него; заодно
позу покоя в запечке стал отдавать pop_chomp_pose.

Одна точка лечит все пять путей запечки, включая вход в комнату.
Резидент +28 Б, скорость не затронута.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:53:44 +03:00
snark13 214d3a822a Уровни 4-6 прошли предварительный тест
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:09:03 +03:00
snark13 671c947ce9 impl_diff: тень спрайтами Кида — осознанное расхождение, не баг
XOR-приём оригинала требует чтения видео-ОЗУ (нельзя) и несовместим с
0xFF-прозрачностью; план — отдельный атлас тени.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:06:19 +03:00
snark13 af4e64455e Кусок узора на стене: модификатор стены не переводился при загрузке
Флаг «no blue» лежит в нулевом бите модификатора стены В ФАЙЛЕ уровня, а
отрисовка (как и оригинал) ищет его в СТАРШЕМ: load_alter_mod переводит
их сдвигом на 7.  Ветку стен мы пропускали намеренно — связи кладки
считаются по соседям, — но noblue из соседей не выводится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:01:07 +03:00
snark13 085a198c20 Мигание тени: кадр приходил из кэша, а снимок писался по Guard.frame
Тень на уровне 6 стояла на двух страницах дабл-буфера в разных позах.
Отрисовка брала image из кэша kid_frame/pop_gframe, который наполняет тик,
а снимок пропуска кадра писала по Char.frame — расхождение застревало
навсегда, потому что снимок совпадал и страница больше не перерисовывалась.

Кадр теперь грузит сама отрисовка, как в оригинале (add_*_to_objtable →
load_fram_det_col, seg008:22F0/2324).  Разбор — BUGS_CLOSED.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:48:45 +03:00
snark13 89b603ae04 Дворцовый портал: Кид скрывался за кромкой раньше времени
Уровень 4: в анимации ухода на следующий уровень контур Кида обрезался не
правой гранью портала, а раньше — брался клип подземного проёма, а
дворцовый шире.

draw_leveldoor считал кромку как xh*8 + 48, без дворцовой поправки. В
оригинале строкой ниже стоит (seg008:1429):

    if (custom->tbl_level_type[current_level]) leveldoor_right += 8;

Значение читает clip_char как правую границу клипа персонажа — отсюда
ранняя обрезка. Расхождение было осознанным и отложенным: в коде стоял
комментарий «+8 у palace-уровней — на уровне 1 не применяется», дворцовых
уровней тогда в порту не было. tbl_level_type[4] = 1, там и проявилось.

pop_palace выставляет pop_bg_load из того же tbl_level_type, что читает
оригинал, так что эквивалент дословный.

Попутно найдено и НЕ починено (заведено отдельным багом
LEVELDOOR-STARTROOM-WIPE): в той же функции оригинал в СТАРТОВОЙ комнате
кладёт затирающий прямоугольник вместо лестницы, со своей дворцовой/
подземной разницей 48/39 и сдвигом 2 px, а мы рисуем марш 144 безусловно.
Видно только при приподнятой створке входной двери, поэтому на обходах
уровней 1-4 не попалось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:27:22 +03:00
snark13 188469d574 Уровень 3 принят: критичных багов нет
Обход после оптимизации фаз: третий уровень прошёл без правок — в
отличие от первого и второго, чинить ничего не пришлось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:04:40 +03:00
snark13 0cae2cb32a QuickSave: носитель — файл, а не EMM-страница
Пересмотр по вопросу пользователя.  Первая редакция плана рекомендовала
EMM-страницу — ошибка: взвешивала скорость и недооценивала главный
сценарий.

EMM-страница не переживает рестарт программы, а именно рестарт — тот
случай, ради которого QuickSave и нужен: сцену каскада плит на 13/23
воспроизводит ТОЛЬКО ESC → запуск заново (perf_l13_room23.md §1).  Снимок
в ОЗУ там не помогает вовсе.

Доводы за EMM при перепроверке оказались слабыми: лимит манипуляторов DSS
ни при чём (один файл, гард _fd_guard и так стоит), а экономия на пути к
файлу — одна строка.  Разница в скорости некритична: 1,9 КБ на HDD не
заметны на фоне полной перерисовки комнаты при загрузке.

Добавлен шаг QS0 — проверить, что D: вообще пишется из-под MAME: если
образ только на чтение, это меняет весь план, поэтому идёт первым.
Критерий приёмки задачи: сохранить, выйти, запустить заново, загрузить —
и оказаться там же.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:01:14 +03:00
snark13 78b93a6b5e План QuickSave/QuickLoad: разбор SDLPoP + инвентаризация нашего состояния
Только изучение и план, кода нет.

Первое, что выяснилось: в оригинале 1989 года быстрого сохранения НЕТ
вовсе — это enhancement SDLPoP (seg000.c, USE_QUICKSAVE, F6/F9).  Значит
искать в Apple II / MSDOS нечего, и повторяем мы не букву, а устройство.

Что берём у SDLPoP: плоский снимок с ОДНИМ обходом на запись и на чтение
(#define process(x)); совместимость держится строкой версии и ничем больше;
клавиша только взводит флаг, работа идёт между кадрами; состояние отрисовки
не сохраняется вовсе — комната перерисовывается с нуля.

Чем наш случай тяжелее: уровень в EMM-странице, room_modif/trobs — static в
банковом pop_trob.c, ГСЧ у нас ТРИ (pop_t_seed, trob_seed, pop_fight_seed),
и дабл-буфер требует перерисовать после загрузки ОБЕ страницы.

Снимок ≈1,9 КБ, поэтому основной носитель — EMM-страница (мгновенно, мимо
DSS и его лимита манипуляторов), файл вынесен в необязательный шаг QS6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:56:27 +03:00
snark13 f44663040c Регресс тактов на 23/13 после обхода уровней 1-2: изменений нет
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).

                работа     синяя   зелёная      циан
af189a1        916 458   142 830   546 900   270 510
40f0d46        873 930   158 874   417 630   379 482
ec1f384        878 550   158 880   419 526   379 488

Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона.  Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.

Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000.  Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:49:02 +03:00
snark13 ec1f384688 Уровень 2 принят; в README — оговорка, что иммортал только в бою
Обход уровней после оптимизации фаз: уровни 1 и 2 пройдены, критичных
багов не осталось.  Найденное по дороге закрыто и перечислено в шапке
TASKS_OPEN.md, чтобы регресс-база была видна одним взглядом.

README раньше читы не перечислял вовсе.  Теперь у бессмертия явно записано
главное ограничение — работает ТОЛЬКО с мечом в руке — и почему оно не
косметическое: кадры seq_74 все с мечом, и подмена смерти на эту анимацию
оставляла Кида в стойке при sword == 0, где у control() нет ни одной ветки.
На физику (падения/пики/чомперы) бессмертие действует всегда.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:42:06 +03:00
snark13 18f58439a3 Боевая стойка без меча: чит бессмертия ставил seq_74 не глядя на меч
Кида сталкивают с ряда 1 на ряд 2, страж падает за спину — Кид встаёт в
боевую стойку без меча и перестаёт реагировать на клавиши.

Оба симптома — один отказ: control() уходит в control_with_sword только при
sword == 2, а для кадров стойки (158/170/171) среди обычных веток нет ни
одной.  Разворот к сопернику за спиной (SEQ_60 при char_opp_dist() < -4)
живёт как раз внутри control_with_sword.

Корень — наш чит, а не механика.  hurt_by_sword перехватывался бессмертием
в самом начале и безусловно ставил SEQ_74_HIT_BY_SWORD, а это анимация
«получил удар В БОЕВОЙ СТОЙКЕ», её кадры 150..179 все с мечом.  В оригинале
(seg002) туда попадают только из ветки sword == 2; безоружного там убивают:
«Being hurt when not in fighting pose means death», take_hp(100).

Спецветка чита удалена целиком — она избыточна: под бессмертием
pop_take_hp(1) и так возвращает 0, и обычный путь сам даёт seq_74 с
сохранённым мечом.  Осталось запретить читу действовать в безоружной ветке.
Глушится вызов, а не pop_take_hp: тот общий с физикой (падения/пики/
чомперы), там бессмертие обязано работать при любом положении меча.

Проверено в MAME: в бою удары урона не приносят, без меча — смерть с одного
удара.  Разбор и грабли метода — BUGS_CLOSED.md#bug-cheat-imm-1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:34:46 +03:00
snark13 aa845c0362 Зелёная фаза: записана идея G8 — пометка соседа узкой полосой
Идея пользователя 2026-08-17.  При падении плиты помечаются ДВА тайла, и
сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО (draw_tile для него зовётся дважды),
хотя потревожили у него только левые 28 px — там, куда свисает правая грань
упавшего тайла.

В записи разведено, что 60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес (сужать
нельзя), и сузить можно только пометку «изменился мой ЛЕВЫЙ сосед».  Плюс три
условия: pop_floor_bake общая (кнопка/зеркало/предмет/щебень) и нужен
отдельный вход; вертикальные диапазоны двух запечек НЕ совпадают (20 строк
против 39), поэтому просто снять вторую пометку нельзя; ширину полосы брать
по максимальному свесу.

Брать ПОСЛЕ обхода всех уровней — решение пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:50:56 +03:00
snark13 dd40c24e7a Чёрные бары поверх стража: заливка запечки уезжала за правый край экрана
Найдено пользователем (2026-08-17, уровень 2 комната 4): после падения плит
(1,7) и (1,8) поверх стража в (1,0) появляются два чёрных бара, большой и
маленький, и МЕРЦАЮТ — то есть живут только на одной странице дабл-буфера.
Пока плиты не упали, баров нет.

Диагноз пользователя оказался верным дважды — и в причине, и в механизме.

ПРИЧИНА.  Падение плиты (1,8) помечает соседа (1,9), и pop_floor_bake заливает
там прямоугольник ШИРИНОЙ 60 от x = 288 — то есть 348, на 28 пикселей за
экран.  Ширина «свой тайл + свес соседа» (40/60/64 при шаге тайла 32) верна
для любой колонки, кроме последней.

А заливка по контракту НЕ КЛИПУЕТ, и это правильно: проверка координат стоила
бы дороже самой заливки (шапка _gfx_recthfill256: «Клиппинга НЕТ, координаты
обязаны быть валидны»).  Значит обрезать обязан ВЫЗЫВАЮЩИЙ — bar не трогаем.

МЕХАНИЗМ МЕРЦАНИЯ (объяснение пользователя).  Страница 1 начинается на 320
байт дальше страницы 0, поэтому запись в страницу 0 с x > 320 попадает в
страницу 1 по x−320.  Отсюда и «бар только на одной странице».  Хуже того,
такая же запись НА странице 1 уходит уже за пределы обеих страниц — в то, что
лежит дальше (палитры и прочее), так что баг не только косметический.

ПОДТВЕРЖДЕНИЕ АРТЕФАКТОМ.  Трасса всех вызовов pop_bar_black на прогоне:

    BAR x=288 y=117 w=60 h=39 ret=D754   <- pop_floor_bake, 288+60 = 348

и независимо снятое расхождение страниц: РОВНО x 0..27 (348−320 = 28 px) при
y 117..155 — то есть в точности y этого бара (yb+26 = 117, высота 39).

ФИКС.  Хелпер bake_w(x, w) обрезает ширину по правому краю (и отдаёт 0, если
прямоугольник целиком за экраном); применён во всех точечных запечках —
pop_floor_bake, pop_ceil_bake_empty, pop_loose_bake_empty.  Обрезка ничего не
теряет: правее 320 восстанавливать нечего.  Копия второй страницы (bake_copy)
берёт ту же обрезанную ширину, иначе запечка и копия разъехались бы.

Заодно, того же рода:
- pop_torch_wipe у факела в колонке 9 заливал x = 328, то есть ЦЕЛИКОМ за
  экраном — теперь пропускается;
- pop_loose_bake_empty читал POP_COL_XH[col + 1] БЕЗУСЛОВНО, то есть у
  последней колонки за концом массива (10 элементов); чтение убрано под
  проверку.

Отсечено по дороге (проверками, не рассуждением): копия запечки не виновата
(сборка с выключенной копией — бары остались, проверил пользователь).

8 наборов tests-host зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:46:09 +03:00
snark13 40f0d46542 Регресс тактов после четырёх фиксов: убран +130 000 в heal и +130 000 в циане
Замер на сцене каскада (ур.13 к.23) ПОСЛЕ багфиксов показал регресс, причём
в том числе в СИНЕЙ фазе, которую фиксы не трогали:

                    до фиксов   после фиксов   после этой правки
    работа            783 798      1 038 528             873 930
    синяя             142 830        275 538             142 830
    зелёная           414 456        415 284             417 630
    циан              269 538        479 628             379 482
    период (растр.)         4              5        4 (один кадр 5)

Разбор синей зондом L (граница «ввод+heal» / «логика») сразу указал место:
логика 88 020 не изменилась, а «ввод+heal» вырос 55 000 -> 187 500.  Это
pop_fore_heal чистит ОБЪЕДИНЁННЫЙ ov_mark: кромка потолка (полоса y 0..8) и
оверлеи соседей (64x64 на тайл) от шести кусков сливались в прямоугольник на
полкомнаты.

Правки, каждая с замером:

1. Оверлей соседа рисуется в ОКНЕ = прямоугольник самого куска.  Смысл
   оверлея — перекрыть кусок, а что лежит вне него, и так нарисовано
   правильным фоном.  Даёт три вещи разом: пикселей в разы меньше; куски
   тайла, не задевающие кусок, отсеиваются предфильтром pop_blit_b даром (это
   и есть вертикальный гейт, отдельного не надо); ov_mark становится НЕ НУЖЕН
   — всё нарисованное лежит внутри heal-коридора куска, а он чистится каждый
   кадр.  Для этого заведён ov_suppress.

2. Кромка потолка — тоже без ov_mark: её куски рисуются от dby = 2, то есть
   занимают строки 0..2, а помечает её только кусок и только пока достаёт
   (mob_y <= 18) — значит она внутри его же коридора.

3. Предусловия перенесены в РЕЗИДЕНТ, до вызова в банк 2: pop_mob_overlay_tile
   и разбор пометок живут в других банках, и каждый вызов платит межбанковый
   трамплин (~8 900), а условий всего два дешёвых чтения тайла
   (pop_tile_code — резидент, ~1 100).  Гейты: тайл соседа не пуст и его
   габарит пересекается с габаритом куска; для полосы потолка — тайл полосы
   не пуст (а он там чаще всего пуст: кусок существует ровно потому, что
   плита из своей клетки выпала, и пустой тайл в переднем слое не рисует
   ничего).

ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ, откачен: сдавать пометки ряда −1 одной МАСКОЙ
вместо до двенадцати отдельных вызовов (один трамплин вместо двенадцати) —
873 930 -> 879 276, то есть хуже.  Пометки ставятся редко, а цена копилки и
разбора маски платится всегда.

Итог: все три секции в бюджете 400 000 (синяя 158 880, циан 379 482), зелёная
417 630 — она была такой и ДО фиксов, это не регресс.  Период вернулся к 4
растровым кадрам; ровно один логический кадр из 54 идёт за 5 (работа 873 930
при пороге 860 000).

Цена четырёх багфиксов по итогу: +90 000 работы за кадр (+11 %), из них весь
рост в циане.

Тестовый образ теперь всегда собирается LEVEL=1 (решение пользователя:
переход на нужный уровень он делает сам).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:14:35 +03:00
snark13 e4e9836f29 Залипший блеск меча: trob_drawn обнулялся, а комната рисовалась по живому modif
Найдено пользователем (2026-08-17, уровень 1 комната 15): при входе в комнату
блеск меча иногда горит несколько секунд вместо одиночной вспышки.  Повторить
не удавалось.

Порядок в enter_room:
  1) pop_trob_room_changed() обнуляет trob_drawn[] для ВСЕХ тайлов
     = «нарисовано без блеска»;
  2) pop_trob_anim_room() задаёт мечу СЛУЧАЙНУЮ фазу счётчика 0..31
     (start_anim_sword, seg007:087C);
  3) pop_room_draw() рисует комнату по ЖИВОМУ modif.

Если случайная фаза попала ровно на 1, комната рисуется С БЛЕСКОМ (draw_tile
берёт кадр (modif==1)+10), а trob_drawn говорит «блеска нет».  На следующем
кадре animate_sword уводит счётчик с 1, признак снова 0 — и СОВПАДАЕТ со
стоячим trob_drawn, поэтому перерисовка не помечается.  Блеск остаётся
запечённым в фон.

Оба наблюдённых числа сходятся:
  вероятность   ровно 1/32 на вход в комнату — отсюда «редко»;
  длительность  animate_sword считает ВНИЗ и на нуле берёт новый период
                0x28..0x67, то есть до 103 логических кадров ~ 6 секунд.

Фикс: trob_drawn для меча инициализируется тем, что реально нарисует комната
(mod == 1), а не нулём.  Одно присваивание на вход в комнату — в кадре ноль.

ПОДТВЕРЖДЕНО АРТЕФАКТОМ, а не рассуждением: временная сборка с подменой фазы
на mod[i] = 1 давала баг на КАЖДОМ входе в комнату 15 (пользователь
подтвердил), с фиксом — ни на одном.  Временный форсаж снят.

У КНОПКИ той же дыры нет, хотя механизм общий (оба используют trob_drawn):
её признак — ДЛИТЕЛЬНОЕ состояние (нажата/нет), рассогласование само
исправляется на первой же смене.  У блеска признак — одиночная вспышка в
ОДНОМ кадре из 40..103, и пропущенная смена стоит целого периода.  Записано в
комментарии, чтобы их не «унифицировали» обратно.

8 наборов tests-host зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:47:26 +03:00
snark13 6a91b9c688 Сосед поверх падающей плиты: не хватало set_redraw2 из draw_mob
Найдено пользователем (2026-08-17, уровень 1 комната 12, стоп-кадр):
плита (0,2) отвалилась и накрыла собой переднюю кромку пола (0,3).

Разбор по стоп-кадру: mobs[0] = {x 64, y 68, row 1, col 2, defer 1}, то есть
кусок в тайле (1,2), а страдает (0,3).  Тайл (0,3) в комнате 12 — КНОПКА
(0x0F), и у неё fore_id = 0.  Проверено по tile_table оригинала
(seg008.c:43) — там ровно то же, наша таблица совпадает байт в байт.  То есть
через ПЕРЕДНИЙ слой эту кромку вернуть нельзя, и вопрос «может, у кнопки в
оригинале есть fore?» закрыт: нет.

Механизм в оригинале другой — draw_mob (seg007:1149) ставит соседу ДВЕ
пометки:

    set_redraw2(tilepos, 1);       // draw_other_overlay -> MIDtable
    set_redraw_fore(tilepos, 1);   // draw_tile_fore     -> FOREtable

Мы портировали только вторую.  А перекрывает кусок именно ПЕРВАЯ:
draw_other_overlay кладёт части соседа в midtable (seg008:1507/1512), тогда
как кусок сидит в objtable, и тот вливается в midtable при обходе СВОЕГО
тайла.  Обход идёт ряды 2,1,0, колонки 0..9 — тайл (0,3) идёт ПОЗЖЕ тайла
(1,2), значит его оверлей ложится поверх куска.

Условие тоже сходится: draw_other_overlay рисует, когда тайл СЛЕВА пуст, —
а (0,2) стал пустым ровно потому, что плита оттуда и упала.

Порт: сам draw_other_overlay у нас уже есть (other_overlay_tile, применялся к
персонажу через redraw_at_char2), ему не хватало только ориентира «тайл
объекта».  Новый вход pop_mob_overlay_tile подставляет тайл куска в
pop_bg_obj_* (с сохранением и возвратом — их читает ещё и mob_tick_one) и
зовёт other_overlay_tile; вся проверка порядка обхода остаётся внутри него.
Зовётся сразу после mob_render: списков у нас нет, порядок эмулируется
последовательностью вызовов.

По бюджету работа появляется только когда сосед слева пуст, то есть в кадрах
сразу после падения плиты, и попадает в циановую фазу (там запас).

8 наборов tests-host зелёные.

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