Две правки, обе про ложные срабатывания пропуска отрисовки персонажа.
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>
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.
Проверка по памяти машины подтвердила и уточнила:
страж, спрайт 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>
Постановка пользователя: проверять, нужна ли отрисовка стража, когда он
не двигается. Если движется — лишние ~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>
Циан 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>
Замер отделил луч от 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>
Разбор 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>
Позиция заводилась с НЕПОЛНЫМ диагнозом. Я приписал 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>
Синяя фаза (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>
Замер 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>
Новая целевая сцена: уровень 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>
Уровень 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>
Порт правила оригинала, разобранного в 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>
СИМПТОМ (пользователь, ур.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>
Правый конец падающего куска лежал поверх соседней плиты. У оригинала
мид-оверлей — 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>
Уровень 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>
Уровень 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>
Найдено и закрыто по дороге: GUARD-RESPAWN-COL0 (c40ae3f) — страж при
возврате в комнату телепортировался в колонку 0 и падал насмерть;
SEAM-FIGHT-FLICKER (a498255) — бой у шва перерисовывал комнату
туда-обратно, поведение сверено с SDLPoP покадрово.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Инструмент: 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>
Пользователь: при бое на шве (Кид в 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>
Уровень 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>
Постановка пользователя: ручной проход уровня с логом фаз + комната/тайл,
дальше оптимизация конкретной комнаты. Записано вместе с блокером —
брейкпоинтами это делать нельзя (эмуляция падает в проценты от реального
времени, играть невозможно); варианты: тап на порт бордюра в Lua-мосте
(не привязан к адресам кода) или самозамер программой в растрах (работает
и на железе).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено и закрыто по дороге: SPIKE-BAKED (980d48c), DIED-ON-BUTTON
(6feab5d). Регресс после них: сцена 13/23 (каскад плит) отрабатывает
штатно, host-тесты 5106 проверок без расхождений — включая 1730 трасс
физики, которых касалось снятие раннего выхода для трупа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 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>
Кид гибнет на кнопке открытия решётки (уровень 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>
Уровень 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>
Флаг «no blue» лежит в нулевом бите модификатора стены В ФАЙЛЕ уровня, а
отрисовка (как и оригинал) ищет его в СТАРШЕМ: load_alter_mod переводит
их сдвигом на 7. Ветку стен мы пропускали намеренно — связи кладки
считаются по соседям, — но noblue из соседей не выводится.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тень на уровне 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>
Уровень 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>
Обход после оптимизации фаз: третий уровень прошёл без правок — в
отличие от первого и второго, чинить ничего не пришлось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пересмотр по вопросу пользователя. Первая редакция плана рекомендовала
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>
Только изучение и план, кода нет.
Первое, что выяснилось: в оригинале 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>
Обход уровней после оптимизации фаз: уровни 1 и 2 пройдены, критичных
багов не осталось. Найденное по дороге закрыто и перечислено в шапке
TASKS_OPEN.md, чтобы регресс-база была видна одним взглядом.
README раньше читы не перечислял вовсе. Теперь у бессмертия явно записано
главное ограничение — работает ТОЛЬКО с мечом в руке — и почему оно не
косметическое: кадры seq_74 все с мечом, и подмена смерти на эту анимацию
оставляла Кида в стойке при sword == 0, где у control() нет ни одной ветки.
На физику (падения/пики/чомперы) бессмертие действует всегда.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кида сталкивают с ряда 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, уровень 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>
Замер на сцене каскада (ур.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, уровень 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, уровень 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, уровень 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>
Регресс от копии второй страницы (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>
зелёная пик 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>
Идея пользователя: то, что пишется в ДВЕ видеостраницы, на вторую можно
копировать, а не считать заново — тем же приёмом, что при перевороте экрана
(зелёное зелье), только без зеркала.
Точечные запечки ставятся с 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>
ПОПРАВКА К 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>
Держали константные 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>
Три части падающей плиты (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>
Сцена — ур.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>
Контрольный замер перед оптимизацией (задача 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>
Точки на цепочку вызовов (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>
Замер (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>