Commit Graph

358 Commits

Author SHA1 Message Date
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
snark13 7ae691b070 Fore потолка поверх падающей плиты: в разборе пометок не было ряда -1
Найдено пользователем (2026-08-17, уровень 1 комната 6): падающая плита
(-1,5) перекрывает собой кромку потолка (-1,6), чего физически быть не может.

Дыра архитектурная: pop_fore_needed обходил только ряды 0..2, ряда -1 в
переднем слое не было вовсе.

Сверено с оригиналом.  redraw_needed_tiles (seg008:1B06) обходит ряды 2,1,0, а
ПОТОМ отдельным проходом ряд 2 комнаты сверху (redraw_needed_above), и его
draw_tile_fore кладёт куски в FOREtable.  Падающая плита идёт в MIDtable
(draw_mobs).  draw_tables рисует back -> mid -> fore (seg008:1373), поэтому у
оригинала кромка потолка оказывается поверх плиты сама собой.

Порт:

- pop_fore_needed: проход по ряду -1 добавлен и идёт ПОСЛЕДНИМ, как в
  оригинале.  Свой набор пометок (rdfa/rdfa_pending) — как и у самих
  перерисовок ряда -1 (rda_*), это отдельный проход, а не 11-я колонка;
- новый лист pop_ceil_fore_tile_b (pop_bg.c) — тот же redraw_needed_above,
  что уже рисовался над персонажем (ceil_over_kid_tile), плюс окно клипа
  ровно на полосу столбца и ov_mark (полоса идёт банком без тени, на второй
  странице её восстановит pop_fore_heal);
- mob_mark_neighbour помечает ряд -1, пока кусок достаёт до кромки.  Кромка
  живёт в трёх верхних строках поля (dby = 2 при клипе по POP_YOFF), спрайт
  куска занимает mob_y-16 .. mob_y, отсюда условие mob_y <= 18 — три кадра
  после отрыва (y = 2, 5, 11 при ускорении 3).  Помечаются ОБА столбца,
  которые кусок накрывает по x (mob_x .. mob_x+62 = col и col+1); именно
  поэтому страдал сосед.

По бюджету работа появляется только в эти три кадра на кусок и попадает в
циановую фазу, где сейчас запас (270 тыс. из 400 тыс.).  Замер на ур.13 —
следующим шагом.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:17:04 +03:00
snark13 bb7cf30910 Зелёная фаза: записаны ДВА условия корректности копии второй страницы
Новый раздел §4: копия годится только если (1) запечка ограничена
копируемым прямоугольником — иначе то, что легло вне него, на второй
странице остаётся прежним и страницы расходятся (мерцание торцов, ур.1
к.6, кнопка (0,2)); и (2) содержимое тайла между двумя кадрами не
менялось — у кнопки с идущим таймером пометка обновляется каждый кадр.

Перечёркнут прежний вывод «окно клипа в pop_floor_bake — только вред»:
по скорости да, но оно ОБЯЗАТЕЛЬНО как условие (1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:05:29 +03:00
snark13 35b7cd5196 Фикс мерцания торцов: копия запечки требует ОКНА КЛИПА
Регресс от копии второй страницы (5ef721e), найден пользователем: уровень 1,
комната 6, Кид на кнопке (0,2) — дрожит нижняя грань переднего торца кнопки.
На уровнях 1-3 то же по торцам плит, полов и кнопок.

Причина системная.  Полная запечка зовёт draw_tile, а тот рисует тайлы
ЦЕЛИКОМ, то есть пишет ШИРЕ прямоугольника бара.  bake_copy переносит на
вторую страницу ровно бар — и всё, что легло вне него, на второй странице
остаётся прежним.  Страницы расходятся, это и есть мерцание через кадр.

У полосы потолка (pop_ceil_bake_empty) проблемы не было: там окно клипа
поставлено ещё в G1, и запечка ограничена ровно копируемым прямоугольником.
А у pop_floor_bake окна не было — до этого оно трижды отвергалось как
невыгодное по скорости.

Фикс: окно клипа в pop_floor_bake возвращено, но теперь это условие
КОРРЕКТНОСТИ, а не оптимизация — записано в коде, чтобы его не сняли снова
«как убыточное».  Само окно стоит ~8 500 такта (клипованный путь дороже
быстрого на ~1 500 на каждом из 7,6 блитов), но открывает копию второй
страницы, экономящую ~145 000: пара «запечка + копия» вдвое дешевле двух
запечек.

Второй гард того же захода: pop_set_redraw / pop_set_redraw_above гасят слот
копии при ПЕРЕпометке тайла — у кнопки с идущим таймером связи пометка
обновляется каждый кадр, и картинка каждый раз другая, копия старой не
годится.  Объявления pop_bake_slot_reset* без __banked: pop_redraw.c и
pop_room.c в одном банке, трамплин не нужен.

Заодно починен скрипт рестарта MAME: он оставлял недоеденный запрос `exit` в
очереди IPC моста, и НОВЫЙ экземпляр его подхватывал и сразу выходил.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:04:09 +03:00
snark13 18ee60eb69 mob_tick_one в file-scope + снятие временной оснастки замеров
зелёная пик  423 558 -> 414 456
  работа пик   793 266 -> 783 798
  ПЕРИОД логического кадра в каскаде: было 5-6 растровых, стало РОВНО 4

mob_tick_one: рабочие переменные в file-scope (было 16 байт кадра и 99
обращений `-N(ix)`, стало 17) — то же лечение, что у draw_tile и blit_b_clip.

Снята временная оснастка из ГОРЯЧИХ путей: шесть вызовов pop_dbg_b1..b6 в
pop_blit_b (по ~65 такта каждый на КАЖДЫЙ блит), pop_dbg_kind/m16 и
подсчёт состава кадра в pop_redraw_needed, pop_dbg_m13..m15 в
pop_ceil_shake_draw.  Сами пустышки в pop_state.c оставлены — вставить их
обратно на один замер дешевле, чем заводить заново; как это делается,
записано в docs/perf_l13_room23.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:47:20 +03:00
snark13 5ef721e340 Вторая страница запечки — КОПИЕЙ с первой (зелёная 546 900 -> 423 558)
Идея пользователя: то, что пишется в ДВЕ видеостраницы, на вторую можно
копировать, а не считать заново — тем же приёмом, что при перевороте экрана
(зелёное зелье), только без зеркала.

Точечные запечки ставятся с pages = 2, срабатывают два кадра подряд (по разу
на страницу дабл-буфера) и оба раза считают одно и то же.  А после первого
раза нужный прямоугольник уже лежит в ОЗУ-копии первой страницы:
gfx_copy_page берёт источником ОЗУ-копию НЕактивной страницы (то есть ЧИСТЫЙ
фон — спрайты рисуются банком без тени и в копию не попадают), а приёмник
обновляет и в видео-ОЗУ, и в ОЗУ-копии.

  запечь щебень (RD_FLOOR)        179 914 -> 107 844 в среднем (копия ~35 000)
  запечь колодец (RDA_CEIL_GONE)  138 318 ->  80 810 в среднем (копия ~17 500)
  зелёная пик                     546 900 -> 423 558
  работа пик                      916 458 -> 793 266

Слот на тайл (bake_pg / bake_pg_above) помнит, НА КАКОЙ странице сделана
первая запечка.  Копируем только если первая была на ДРУГОЙ странице и
дабл-буфер включён; иначе честно пересчитываем.  Такая проверка выдерживает и
переплетение двух запечек в одном кадре, и однобуфер (чит SPACE), и смену
комнаты (pop_bake_forget в enter_room — номера тайлов повторяются).

Проверено: 8 наборов tests-host зелёные.  Мерцания через кадр нет — шесть
снимков подряд после каскада попиксельно совпадают в поле, различаются ТОЛЬКО
полосы профилировочного бордюра (снимок ловит разную строку растра);
пользователь подтвердил визуально.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:32:35 +03:00
snark13 14e183108d Документы по фазам: результаты дня и оставшийся план
Зелёная и циан — раскладка ПОСЛЕ правок, журнал правок с коммитами,
отрицательные результаты с объяснением причины (окно клипа в
pop_floor_bake — три попытки), и что осталось: раскол draw_tile на девять
узких частей по образцу seg008:01C7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:23:38 +03:00
snark13 af189a1d92 Композит куска: точный габарит + ИСПРАВЛЕНИЕ размеров в b2da0b8
ПОПРАВКА К b2da0b8.  Там я снял размеры частей падающего куска из каталогов
атласов с НЕВЕРНЫМ сдвигом раскладки (page = id>>5 вместо id>>4 —
POP_ENV_SHIFT равен 4).  Настоящие размеры:

    оба тайлсета   70 = 32x13   74 = 32x3
    72 = 26x16 в подземелье, 25x16 во дворце

То есть прежний комментарий в mob_render (32x13 / 32x3 / 26x16) был ВЕРЕН, а
«исправление» — нет.  Следствия:

- НИКАКОГО БАГА КОРИДОРА НЕ БЫЛО: след куска по x — mob_x .. mob_x+57, и
  старый коридор mob_x-4 .. mob_x+59 его покрывал.  Заявление про «три
  пикселя, не стиравшиеся во дворце» неверно, снимаю.
- Сам коридор оставляю как стало (mob_x .. mob_x+63): площадь та же, но
  четыре пикселя запаса переехали слева, где кусок не рисует ничего, вправо,
  где их было всего два.  Это не исправление бага, а перекладка запаса.
- КОД во всех случаях работал правильно: он читает раскладку через
  POP_ENV_SHIFT/POP_ENV_MASK, ошибка была только в моём анализе.

Само дело: композит собирается с ТОЧНЫМ габаритом вместо буфера-максимума.
Габариты частей читаются первым проходом (у тайлсетов правая часть разная),
из них считаются ширина и высота блоба, и страйд равен ширине.  Раньше блоб
объявлялся 63 px шириной при фактических 58 — пять прозрачных колонок
переносились на каждом кадре каждого куска.

  циан пик    273 108 -> 270 510
  работа пик  920 862 -> 916 458

Проверено: 8 наборов tests-host зелёные; кадр с четырьмя плитами в воздухе
совпадает пиксельно с прежним.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:20:29 +03:00
snark13 9a50ab20d7 Коридор heal падающего куска — по высоте композита (-12 тыс. на кадр)
Держали константные 24 строки «на всякий случай», хотя собранный композит
знает свою высоту: у дворца 16 строк, у подземелья 19.  Теперь коридор =
высота композита + две строки сверху и одна снизу (mob_heal_up/mob_heal_h
ставит mob_spr_build).  На самой дорогой операции кадра, помноженной на шесть
падающих плит:

  pop_loose_mob_tick   168 600 -> 156 762
  зелёная пик          557 706 -> 546 948
  работа пик           931 782 -> 920 862

Плюс ТРЕТЬЯ проверка окна клипа в pop_floor_bake — снова хуже (179 914 ->
188 417), и теперь ясно ПОЧЕМУ: детальный зонд по каждому блиту показал, что
клипованный путь стоит +1 500 такта на КАЖДОМ из 7,6 блитов (+11 400), а
режет он только редкие высокие куски (в трассе нашлись два: 34 878 -> 25 872
и 28 818 -> 24 090, всего -13 700), которых в среднем по 12 перерисовкам нет.
Запись в коде: больше не пробовать.

Заодно снят детальный профиль pop_floor_bake (одна перерисовка, 179 914):
  вход                                   2 382
  pop_bar_black 60x39 (вкл. pop_cd_touch) ~15 500
  контекст draw_tile #1                  13 584
  4 блита тайла #1                       50 718
  диспетчер между блитами #1             11 388
  контекст draw_tile #2                  13 584
  4 блита тайла #2                       ~54 000
  диспетчер между блитами #2             11 388
  хвост                                   6 474

Итог по сцене от базового замера:
  работа   1 437 150 -> 920 862  (-36%)
  зелёная    805 000 -> 546 948  (-32%)
  циан       631 800 -> 273 108  (-57%, в бюджете)

Проверено: 8 наборов tests-host зелёные; в MAME кадр с четырьмя плитами в
воздухе чистый (коридор сузился — следов нет), итоговая картинка прежняя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:14:12 +03:00
snark13 b2da0b85b5 Композит падающего куска: циан в бюджете (632 000 -> 273 078)
Три части падающей плиты (env 70/74/72) складываются в ОДИН getimage-блоб
при загрузке тайлсета, и кусок рисуется одним блитом вместо трёх.  Блоб
лежит в обычной памяти (W2), поэтому вызов идёт без atlas_image и без
gfx_w0_map/unmap — ещё ~1 350 такта.  Прозрачность соблюдена: части
ПЕРЕКРЫВАЮТСЯ (74 и 70 обе от mob_x), поэтому композит собирается
попиксельно с пропуском 0xFF, то есть точно как три прозрачных блита.

Мотив: у блита ~8 800 такта постоянных накладных против ~5 000 на пиксели.
Шесть кусков в воздухе = 18 вызовов = 258 708 такта, больше половины
цианового блока.  Резервный путь на три блита оставлен (mob_spr_ok).

  циан пик   435 180 -> 273 078   (цель 400 000 — ВЫПОЛНЕНА)
  работа     1 037 250 -> 931 782
  зелёная      558 498 -> 557 706 (не затронута, ею занимаемся дальше)

Заодно НАЙДЕН И ПОФИКШЕН БАГ КОРИДОРА heal.  Снятые из каталогов реальные
габариты частей оказались другими, чем в комментарии, И РАЗНЫМИ у тайлсетов:

    подземелье  70 = 32x16   74 = 26x15   72 = 26x16
    дворец      70 = 32x13   74 = 25x15   72 = 31x13

То есть во ДВОРЦЕ (уровни 4-6, 10, 11, 13, 14) кусок достаёт до mob_x+62, а
коридор heal был mob_x-4 .. mob_x+59 — правые три пикселя не стирались
никогда.  Четыре пикселя слева при этом чистились впустую: левее mob_x
кусок не рисует ничего.  Коридор стал mob_x .. mob_x+63, площадь та же.
Высоту (24 строки при следе 19) НЕ сужаем: 24 — это след подземелья плюс
две строки поля с каждой стороны, «сужение по палаццовому следу» сломало бы
подземелье.

Ещё три правки того же захода:

- pop_blit_b: аргументы в file-scope.  Третий и дальше SDCC передаёт стеком,
  и каждое чтение шло через `-N(ix)` — 76 обращений.  Стало 11.
- pop_loose_mob_tick: пометки всех кусков одним пакетом (было по 4 502 такта
  на кусок).  mobTk 176 772 -> 168 600.
- pop_floor_bake: окно клипа проверено ВТОРОЙ раз (после того как
  blit_b_clip подешевел) и снова хуже — 182 124 -> 188 460.  Причина
  записана в коде: у этого тайла клипа нет, блиты идут быстрым путём, а окно
  их уводит в клипованный и при этом не отсеивает ни одного куска и не
  режет ни одной строки.  Не пробовать в третий раз.

Новый лист pop_mem_b (резидент): блит блоба из обычной памяти с теми же
клипом полосы у потолка и окном перерисовки, что у pop_blit_b.

Проверено: 8 наборов tests-host зелёные; в MAME кадр с летящими плитами и
итоговая картинка совпадают с прежними.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:01:48 +03:00
snark13 a3c473d910 Оптимизация каскада плит: -27% работы за кадр (пять правок с замерами)
Сцена — ур.13 комната 23, шесть плит с потолка (docs/perf_l13_room23.md).
Все замеры сняты одним и тем же прогоном «ESC -> зонды -> roomtest».

                       было       стало
  работа за кадр    1 437 150   1 051 332   -27%
  зелёная (фон)       805 000     575 730   -28%
  циан (спрайты)      631 800     438 546   -31%
  синяя                142 830     142 830   в бюджете

Цена одной перерисовки:
  RDA_CEIL_GONE (запечь колодец)  251 335 -> 137 975   -45%
  RD_FLOOR (щебень на посадке)    198 805 -> 182 124    -8%
  RDA_CEIL (дрожащая плита)        48 785 ->  43 543   -11%

C1. Пометки «фон трогали» в mob_render ПОДАВЛЕНЫ (pop_cd_mute): коридор
куска уже помечен одним вызовом в pop_loose_mob_tick, а три блита метили
подмножества того же прямоугольника по 4 502 такта.  Плюс пометка в
mob_spawn_copy — кусок, рождённый внутри тика, свою мог не получить
(слот выдаётся с начала таблицы, то есть уже пройденный).  -55 тыс.

G2. Контекст тайла — file-scope, а не локали draw_tile (порт
load_curr_and_left_tile, seg008:0339).  В функции 57 вызовов, и каждое
живое через вызов значение спиливалось: 26 байт кадра и 513 обращений
`-N(ix)`.  Стало 33 обращения, кадра нет, банк 7 -703 Б.  -55 тыс.

G1. Окно клипа для точечной перерисовки (pop_t_win_set/clear).  В
pop_ceil_bake_empty куски ряда 0 высотой 63 px рисовались целиком, хотя
восстановить надо девять строк: блит стоил 21 447, стал 7 619.  -90 тыс.
В pop_floor_bake окно попробовано и ОТКАЧЕНО (там нет клипа, блиты шли
быстрым путём, а окно уводило их в blit_b_clip и не отсеивало ничего:
187 266 -> 198 279) — вернуться после G3, запись в коде.

G3. blit_b_clip: байтовый габарит + file-scope вместо локалей.  Одного
байтового габарита НЕ ХВАТИЛО (кадр остался, 211 -> 173 обращений) —
значений, живых через шесть вызовов ядер libbgi, больше, чем регистров.
С file-scope: 51 обращение, кадр 22 -> 12 Б.  Клипованный блит подешевел.

C4. Падающий кусок КЛИПУЕТСЯ сам, вместо чистки бортов после.  Раньше он
рисовался в борт целиком и взводил border_dirty, а pop_room_clip_borders
стирал две полосы 320x28 — 150 978 тактов в каждом кадре, пока хоть один
кусок торчит выше поля (гряда 13-го рождается ровно у потолка, y=2), то
есть почти весь каскад.  Стало 1 722.  -138 тыс.

Заодно blit_b_oversize больше не ходит через blit_b_clip (у того габарит
теперь байтовый) — рисует напрямую gfx_blit_part.  Это путь под
полноэкранные подложки интро/финала (320x200, docs/perf_l13_room23.md §5).

Проверено: 8 наборов tests-host зелёные; в MAME каскад рисуется корректно
(кадр с летящими плитами, чистые борта, итоговая картинка как до правок).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:39:57 +03:00
snark13 babd40bc84 Профиль каскада плит ур.13 к.23: три рабочих документа по фазам
Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок).  Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.

Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх.  По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.

Что нашлось (всё подтверждено зондами, не гипотезы):

  RDA_CEIL      дрожащая плита-потолок     48 658 x до 6 = 292 000
  RDA_CEIL_GONE запечь колодец            251 023 x до 2 = 619 000
  RD_FLOOR      щебень на месте посадки   198 259 x до 2 = 397 000

Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600.  У blit_b_clip — 22 Б кадра и 211 (ix).

Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.

Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.

Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60.  Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).

Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):

  docs/perf_l13_room23.md  сцена, рецепт воспроизведения, зонды, канал clog,
                           сводка по кадрам, габариты спрайтов
  docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
  docs/perf_cyan_phase.md  циан: раскладка, позиции C1..C7, журнал

Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest).  Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:22:21 +03:00
snark13 c312e4a043 Найдены 8791 тактов на вызов блита: uint16_t габарит в pop_blit_b
Точки на цепочку вызовов (libbgi живёт в резиденте W1, адреса однозначны —
пересборка не нужна) разложили постоянные накладные блита:

  pop_blit_b ДО вызова ядра ....... 6 683   <- НАШ код, 77% накладных
  gfx_blit_noclip + _bgi_begin
    + _gfx_blit_sprite_noclip ..... 1 938
  пролог _bgi_blit_rows_raw ....... 457
  строчный цикл ................... 389/строку  (= 198 + 5,96*32, сходится
                                                 с регрессией)
  эпилог + _bgi_end + возврат ..... 1 355
  «вне цикла» ..................... 8 725 при ЛЮБОЙ высоте (h=9..60)

Причина в pop_blit_b, подтверждена чтением .asm: `w`/`h` объявлены
uint16_t, 16-битные значения не влезли в регистры, и SDCC увёл функцию в
14-байтовый стековый кадр (`ld iy,#-14 / add iy,sp / ld sp,iy`), после чего
`w = img[0] | (img[1] << 8)` развернулось в ДВА ДЕСЯТКА IX-относительных
пересылок между ячейками -7..-13 кадра.

Правка: габарит читается БАЙТАМИ.  Корректно по построению — обе ветки и
так требовали w<256 && h<256 (эти проверки теперь убраны как тождественные),
а кадры атласов не крупнее 32x63; формат .atl допускает больше, такой кадр
уходит на общий путь (blit_b_oversize).

Замер A/B на той же детерминированной сцене, те же выборки:
  gfx_blit_noclip  13 679 -> 11 530  (-16%)
  blit_b_clip      18 853 -> 15 340  (-19%)
Стековый кадр 14 -> 6 байт, _CODE -42 Б.  Тайл ~107 000 -> ~95 000.

Все 8 наборов tests-host проходят, комната 23 в MAME рисуется корректно.

Плюс TASKS_OPEN.md: OPT-BLIT — руководство на следующую сессию (где ещё
uint16->uint8, как искать IX-спиллы по asm, что НЕ делать, и грабли с
несколькими экземплярами MAME на один error.log).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:51:20 +03:00
snark13 b27b31318a Разбор draw_tile: модель цены блита + пакетная пометка pop_cd_touch
Замер (MAME, комната 23 ур.13) разложил цену одного gfx_blit_noclip
регрессией по 339 блитам, высоты 3..63:

    такты = 8791 + 198.2 * h + 5.96 * (w * h)

Модель ложится на весь диапазон (32x3 -> 9 891, 32x13 -> 13 887,
32x60 -> 32 073), разброс внутри размерной группы — ТРИ такта.

Выводы:
- 5.96 на байт — предел железа: байт идёт через акселератор дважды
  (burst src->память акселератора, burst ->экран), по 3 такта на проход
  при системном клоке 21 МГц.  Ускорять передачу нечем.
- 198 на строку — цикл _bgi_blit_rows_raw; при w=32 это половина
  построчной цены.
- 8791 на ВЫЗОВ — крупнейшая статья, НЕ объяснена.  Проверено, что это не
  W3-скобка (в обеих половинах по 5 инструкций) и не прерывания (разброс
  3 такта).  Один тайл = 5-6 спрайтов ~ 107 000 тактов, из них ~50 000 —
  постоянные накладные вызовов.  Это и есть главный резерв.

Заодно: pop_cd_touch собирается в ПАКЕТ на тайл (pop_cd_batch_begin/end,
скобка в draw_tile) вместо вызова на каждый кусок — раньше 2 866 тактов
на спрайт, 15% цены блита.  ВНИМАНИЕ: выигрыш замером НЕ подтверждён —
в захваченных кадрах скобка не сработала (блиты шли из холодной отрисовки
комнаты, мимо draw_tile).  Правка безопасна по построению: объединение
прямоугольников может пометку только расширить, не сузить.

Снятые сегодня неверные утверждения (чтобы не всплыли):
- «клипованный блит дороже полного» — артефакт сравнения разных выборок
  спрайтов; 32x60 с клипом до полосы стоит 7 236, то есть клип работает;
- «блиты идут программным циклом со скоростью ldir» — нет, ядро на
  акселераторе, см. модель выше;
- «отложить запекание на кадр» — НЕЛЬЗЯ: запекание пишет ОЗУ-копию фона,
  из которой heal восстанавливает; отложенное даёт призрак плиты.

Все 8 наборов tests-host проходят.  Оснастка замера временная.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:36:48 +03:00
snark13 f89b7dd0d2 Ускорение холостого хода: указательный обход вместо arr[i] в горячих циклах
Замер зелёного блока (комната 23 уровня 13, MAME, такты эмулятора) показал,
что 86% его стоимости в ПОКОЕ — это pop_loose_tick, который не делает ничего.

Причина — кодоген SDCC, подтверждена чтением .asm.  С `int pos` и записью
pop_loose_modif[pos] компилятор держал счётчик в IX-фрейме, каждую итерацию
заново складывал 16-битный адрес элемента, клал его в локал и тут же
вычитывал обратно парами `pop bc / pop hl / push hl / push bc`.  40 холостых
итераций (30 тайлов + 10 потолков) стоили 27 936 тактов.  То же в
pop_loose_mob_tick: запись mobs[i] заставляла умножать i на sizeof(mob_t)=15
заново под КАЖДОЕ поле (.active/.clean/.x/.y), 14 пустых слотов — 35 790.

Правка — обход указателем, счётчик uint8_t, пустые слоты отсеиваются в
вызывающем цикле (а не гардом внутри mob_tick_one, до которого надо ещё
дойти).  Холостая итерация стала `ld a,(de) / or a,a / jp Z` — три
инструкции вместо дюжины с обращениями к памяти.

Результат (такты MAME, холостой кадр):
  циклы по тайлам      27 936 -> 9 852   (2,8x)
  pop_loose_mob_tick   35 790 -> 7 416   (4,8x)
  pop_loose_tick       70 866 -> 24 384  (2,9x)
  ЗЕЛЁНЫЙ БЛОК         82 242 -> 35 760  (2,3x)
Банки ужались: BANK3 -90 Б, BANK7 -74 Б.

Все 8 наборов tests-host проходят; комната 23 в MAME рисуется корректно.

Плюс ВРЕМЕННАЯ оснастка замера (маркеры m9..m16, pop_dbg_kind) — она же
показала, что пик зелёного блока сидит НЕ в тряске плит, а в запекании
тайлов; разбор продолжается, оснастку снять перед закрытием темы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:08:05 +03:00
snark13 50e4eda2ad Падающие плиты: передний слой по пометкам, честные спрайты, замер кадра
Слой fore переведён на пометки ОБЪЕКТОВ вместо окна вокруг персонажа —
порт set_redraw_fore/redraw_frames_fore (seg007:0550, seg008:214):
pop_set_redraw_fore + pop_fore_needed + pop_fore_tile_b, пометки ставит
падающий кусок вокруг себя (draw_mob, seg007:1147).

Ключевая правка: пометки соседа считаются в ПРОХОДЕ ОТРИСОВКИ, а не в
тике.  Стояли в цикле тика — то есть по позиции ДО mob_tick_one, а кусок
рисовался уже по новой; пока он летит внутри ряда, тайлы совпадают, но в
кадр пересечения границы пометка указывала на прежний тайл и колонна кусок
не перекрывала.  Ровно один кадр, как и наблюдалось.

Спрайты падающего куска: obj_id = 10 (add_mob_to_objtable, seg007:1170),
части берутся по этому индексу — 70/74/72, а не 41/43/42 (плита В ПОКОЕ).
Отсюда была «цельная ровная плита» вместо двух половин со сдвигом.

Убрана подпорка «перерисовать соседний тайл поверх куска»: тащила на плиту
чужой узор и окно и стоила по полной отрисовке тайла на кусок за кадр.
Коридор heal сужен по высоте 32 -> 24 (реальный след спрайта 16 px).
gfx_set_bank вынесен из потайлового цикла в обрамление прохода.

Выяснено и зафиксировано: clip.right = 40 в add_mob_to_objtable — МЁРТВЫЕ
данные.  set_clip_rect вызывается под chtab_flip_clip[chtab_id], а для
окружения флаг равен нулю — оригинал падающие куски не клипует вовсе.
Портировать нечего; две попытки это сделать были ошибочны.

Замер кадра (маркеры m1..m4, оставлены временно для проверки правок):
покой 196 614 тактов (15,2 % кадра при периоде 1 290 000), во время
падения гряды среднее 222 667, пик 1 428 990 — то есть 111 % кадра,
шесть кадров подряд за пределом.  Логика неизменна (88 020), растут фон
(до 887 376) и спрайты (до 660 792).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:38:59 +03:00
snark13 c31930dcad midtable: разобрано, насколько узко оригинал помечает передний слой
set_redraw_fore (seg007:0550) зовут ровно из трёх мест, и нигде нет
пометки «всё»: персонаж помечает прямоугольник своего футпринта
(redraw_at_char, seg003:0576; для Кида — объединение с прошлым кадром),
падающий кусок — соседа справа и второй тайл на границе рядов (draw_mob,
seg007:1147), анимация тайла — один тайл.

Значит пометка переднего слоя НЕ ШИРЕ нашего окна вокруг персонажа, и
полный порт objtable её не отнимает, а формализует.  Главный риск снят.

Побочно найден точный рецепт для MOB-CLIP-RIGHT: оригинал не рисует соседа
поверх куска и не полагается на clip.right — он помечает соседний тайл
парой set_redraw2 + set_redraw_fore, и порядок делает всё сам.  Это же
закрывает «плиту перед колонной».

Рекомендация по итогам: вариант A (полный порт).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:10:55 +03:00
snark13 322d021411 roomnav_skip: достижимость считать обходом от старта, а не входящими связями
Уровень 1 показал ошибку критерия: комнаты 13 и 18 ссылаются только друг на
друга (13.down = 18, 18.up = 13), то есть входящая связь у каждой есть, но
из остального уровня в них не попасть.  Счётчик входящих их пропускал.

Заменено на BFS от стартовой комнаты по связям.  Уровень 1 теперь даёт
ровно {13, 18, 24}, как и должно быть.  Заодно всплыло, насколько мало
комнат реально проходимо на поздних уровнях: 13-й — 11 из 24, 14-й — 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:08:47 +03:00
snark13 69081ae51e Таблица комнат для отладочного телепорта: что пропускать и почему
Посчитано скриптом по res20NN.bin для всех 15 уровней.  Три критерия:
нет входов (ни одна комната не ссылается связями), нет пола (стоять не на
чем) и пол только опасный (обычного пола код 1 нет — пики/расшатанные).

Плюс спецкомнаты, которые пропускать независимо от критериев: seamless
exit 12/23 (меняет уровень), falling exit 6/1, falling entry 7/17,
спуск-через-зацеп 7/14, тень с зельем 5/24 и 13/23+13/16, где вход роняет
гряду плит.  Уровень 15 (защита от копирования) — целиком.

Нужна для полного обхода комнат с первого уровня после порта слоёв.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:04:44 +03:00
snark13 9e1a55bdc3 Падающие плиты: спрайты падающего набора, замеры слоёв, анализ midtable
Спрайты: у падающего куска obj_id = 10 (add_mob_to_objtable, seg007:1170),
и три части берутся из таблиц по этому индексу — 70/74/72, а не 41/43/42.
Мы рисовали плиту В ПОКОЕ, отсюда «цельная ровная» вместо двух половин со
сдвигом правой на пиксель.

Подпорка «перерисовать соседний тайл поверх куска» убрана: она тащила на
плиту чужой узор и окно, да ещё стоила по одной полной отрисовке тайла на
кусок за кадр.  Клип объекта (clip.right = 40) НЕ портирован — единицы поля
не выяснены, буквальные 40 пикселей срезают задний угол плиты.  Цена отказа
— правая грань куска и передние части чужих тайлов его не перекрывают
(MOB-CLIP-RIGHT, BUGS_OPEN.md).

Коридор heal сужен по высоте 32 -> 24: реальный след спрайта 16 px.
Ширина оставлена 64 — из неё используются 62, экономить нечего; заодно в
комментарии зафиксировано, что «выравнивание блока акселератора» ничем не
подтверждается и требует проверки артефактом.

docs/midtable_analysis.md — разбор слоёв оригинала и замеры:
- логический кадр 1 289 526 тактов;
- pop_redraw_needed 978 тактов (0,08 %);
- fore-проход 128 778 тактов (10 %) на ОДНОГО персонажа, но выполняется
  лишь на 4 % кадров — гасит пропуск неизменившегося персонажа;
- максимум объектов на одном тайле 2 (пара из loose_fall), значит
  сортировка внутри тайла бесплатна.

Вывод: единственный риск порта objtable — сохранится ли окно клипа
fore-прохода; до разбора redraw_frames_fore выбирать вариант рано.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:56:56 +03:00
snark13 67d62e3e8b Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв
Уровень 12 (тень) — порт seg002/seg006:
- check_shadow: подъём тени в комнате 15 по условию «меч подобран»,
  init_shad_12, вход падением (seq 7);
- autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ;
- общий урон (ранил тень — ранил себя) и check_killed_shadow
  (убил тень — убил себя);
- таймер вспышки слияния 42 -> -1, sword_disappears при уходе из
  комнаты 18, появление плит в комнатах 2/13 после слияния.

Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом
попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт
сбросить HP.  Чит навигации по комнатам этот триггер придерживает —
иначе комнату 23 двенадцатого уровня не посмотреть в принципе.

Уровень 13 (Джафар): guard_notice_timer (фора после встречи),
on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три
исключения loose-механики.  Плюс спрайты визиря VIZIER.DAT — без них он
рисовался обычным стражем; заодно цвет из данных комнаты применяется
только к обычному стражу, как в оригинале.

Падающие плиты — семь дефектов, найденных прогонами:
- MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние
  терялись без щебня;
- одиночные сигналы посадки/провала больше не затираются в кадре;
- честный loose_fall: сбитая плита рождается в том же кадре, от места
  удара, с половинной скоростью;
- при снятии плиты метится и сосед справа (висел передний торец);
- heal и отрисовка кусков разнесены на два прохода;
- куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y
  (compare_curr_objs для пары 0x80 сортирует наоборот);
- wall_pattern убран из ceil_over_kid_tile: у оригинала узор из
  draw_tile_bottom идёт в фон, а не в передний слой.

Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44).
TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам.

Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами
тени при слиянии, чит навигации не запускает бесшовный переход, чит K
убивает через штатный путь смерти.

Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER,
GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:21:52 +03:00
snark13 716f6f9273 NEXT_SESSION: итоги дня — разгрузка резидента и переворот приняты
Задача на завтра: smoke-прогон уровня 9 + регресс уровней 1-8 (слой фона
теперь весь спрашивает pop_upside).  Записаны раскладка кода по банкам после
разгрузки (куча 902 -> 5795 Б), устройство переворота с замерами, два
правила, которые линкер не проверяет (банк не маппит W3; прямые вызовы
только внутри одного банка), новые тайминги моста и грабли дня — включая
баг SDCC с потерянным `return 1` и разницу логических/экранных координат
при перевороте.

Открытый риск на проверку: имена kid10_v.atl…kid27_v.atl — 9 символов до
точки при DSS 8.3.
2026-08-12 23:49:41 +03:00
snark13 7f5c99ba4f Фикс чистки факелов при перевороте: чистим в СТАРОЙ системе координат
На скриншоте пользователя после U остался чёрный квадрат в стороне от
факела, а у двух факелов пропало пламя.  Причина — система координат:
pop_upside переключается в главном цикле ДО pop_flip_screen, а картинка на
экране к этому моменту нарисована ещё в прежней.  pop_torch_wipe считала
координаты уже зеркально, поэтому чёрный бар ложился в отражённое место
(там и остался квадрат), а само пламя не стиралось.

Чистка теперь идёт при временно возвращённом старом значении pop_upside —
то есть ровно по той картинке, что на экране.
2026-08-12 23:44:22 +03:00