Лёгкая: 628 542 -> 464 796 (-163 746), персонажи не рисуются вовсе. Тяжёлая: 758 358 -> 617 487 (-140 871), рисуется только Кид — он действительно стоит под пламенем, а страж нет. Пятирастровые кадры в тяжёлой позиции исчезли (было 27 %). Итог восьми позиций: 801 768 -> 464 796 в лёгкой (-42 %). До цели 430 000 осталось 35 000 в лёгкой и 187 000 в тяжёлой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
Сцена и замер: факел + чомпер + страж, уровень 11 комната 15
Вторая целевая сцена для оптимизации (первая — perf_l13_room23.md,
каскад плит). Здесь узкое место другое: не разовый пик на каскаде, а
постоянная цена статичной комнаты, в которой одновременно живут два
факела, чомпер и страж.
Такты — totalcycles MAME (не такты Z80, ≈2,4× номинала, memory
sprinter_wait_states_2x). Растровый кадр = 430 000. Хвост кадра — три
gfx_wait_vsync, поэтому логический кадр занимает 3 растра, пока работа
укладывается в один; при работе 1..2 растра период становится 4.
1. Сцена
Уровень 11, комната 15. Проверено чтением состояния машины:
pop_current_level = 0x0B, cur_room = 15.
| кто | где | кадр |
|---|---|---|
| Кид | (0,2), x=98 | 15 (стойка с мечом) |
| чомпер | (0,3) | застывший (trob мёртв) |
| факел | (0,2) — пламя рисуется в ячейке (0,3) | анимируется каждый кадр |
| факел | (0,7) — пламя в ячейке (0,8) | анимируется каждый кадр |
| страж | (0,8), x=170 | 171 (боевая стойка) |
Ни Кид, ни страж не двигаются: сцена статична, разброс замера — сотни тактов на 800 000.
2. Замер (09f32ce, база модуля roomtest.c = 0x42AD)
867 кадров, зонды A/C/D/E; детализация — тремя отдельными прогонами (m1, m5..m7, M/F). Период кадра: 4 растра во всех 866 интервалах.
| участок | зонды | медиана | доля работы |
|---|---|---|---|
| синяя: ввод + heal | A→m1 | 141 048 | 17,6 % |
| синяя: логика | m1→C | 152 190 | 19,0 % |
| синяя, всего | A→C | 293 238 | 36,6 % |
зелёная: pop_loose_tick |
C→m5 | 28 872 | 3,6 % |
зелёная: pop_process_trobs |
m5→m6 | 92 448 | 11,5 % |
зелёная: pop_redraw_needed |
m6→m7 | 190 260 | 23,7 % |
| зелёная: шов/ворота соседа | m7→D | 9 336 | 1,2 % |
| зелёная, всего | C→D | 320 916 | 40,0 % |
циан: check_mirror + loose_mob_draw |
D→M | 37 458 | 4,7 % |
| циан: Кид + страж + fore + HP | M→F | 148 566 | 18,5 % |
циан: char_fore(KID) + борта |
F→E | 1 740 | 0,2 % |
| циан, всего | D→E | 187 758 | 23,4 % |
| работа | A→E | 801 768 | 1,86 растра |
Циан здесь не выделяется: 187 758 — это 0,44 растрового кадра, а разброс за 867 кадров всего 174 такта (187 674..187 848). Впечатление «циан ~150 % кадра» на глаз не подтвердилось — при периоде 4 растра полосы бордюра размазаны по кадрам и на глаз не читаются.
3. Главная находка: чомпер справа от факела — 190 260 тактов/кадр
pop_dbg_rdmax_tot = 1: за кадр перерисовывается РОВНО ОДИН тайл, и
стоит он все 190 260 тактов зелёной фазы.
ПОПРАВКА 2026-08-19 (после реализации P1). Механизм ниже описан верно, но ГЛАВНЫМ источником 190 260 тактов он НЕ был. Зонд
pop_dbg_kindпоказал, что все 312 перерисовок в прогоне — видPOP_RD_CHOMP(полная), и ни одной от факела: собственная пометка чомпера просто перебивала пометку соседа. Настоящая причина — в §6. Урок ровно тот, что уже записан вdefer_unexplained_quirks: механизм, который правдоподобно объясняет цифру, ещё не доказан цифрой.
Цепочка:
TORCH_ANIM_DIV = 1— факел меняет кадр пламени КАЖДЫЙ логический кадр;- пламя запекается в фон (
pop_torch_draw,GFX_BANK_NORMAL), а канвас пламени 16×18 лежит в ячейке правого соседа (seg008:560) — то есть поверх чомпера; - поэтому
pop_process_trobsметит соседа:if (trob_rcode[i] == TILE_CHOMP) pop_set_redraw(tp + 1, POP_RD_CHOMP, 1)(портset_redraw_anim_right); pop_chomp_redrawотвечает на пометку heal 32×64 + полныйdraw_tile.
Расхождение с оригиналом именно в шаге 4. set_redraw_anim_right метит
слой anim, и оригинал возвращает только его (draw_tile_anim_topright →
draw_tile_anim_right → draw_tile_anim) — одну графику чомпера поверх
огня. Мы вместо этого стираем и пересобираем тайл целиком, со всеми слоями
(draw_tile_right, base, bottom, loose), которые пламя вообще не
трогало.
Цена по модели блита (blit_cost_model, 8791 + 198·h + 5,96·w·h):
heal 32×64 ≈ 33 700, значит на один draw_tile уходит ≈ 156 000 — сходится
с известным замером «полная запечка щебня 179 914».
Чомпер при этом застывший: своей анимации у него нет, поза не меняется, возвращать нужно ровно ту же графику поверх свежего пламени.
4. Что это даёт и куда смотреть дальше
Ранжирование по цене (доля от 801 768):
| # | участок | такты | что делать |
|---|---|---|---|
| 1 | redraw_needed: чомпер под факелом |
190 260 | вернуть только слой anim, как в оригинале — без heal и без остальных слоёв |
| 2 | синяя: логика двух Char | 152 190 | графики нет вообще; разобрать pop_check_can_guard_see_kid и два play_seq |
| 3 | циан: Кид + страж | 148 566 | оба будятся каждый кадр — метки фона от чомпера/факела накрывают обоих |
| 4 | синяя: heal двух Char | 141 048 | следствие того же: skip не срабатывает ни разу |
| 5 | process_trobs: два факела |
92 448 | ≈46 000 на факел при блите пламени 16×18 ≈ 14 000 — разобрать накладные |
| 6 | loose_tick |
28 872 | в комнате нет ни одной loose-плиты |
Пункты 3 и 4 — одна тема: пока фон трогают каждый кадр, pop_char_skip_mask
не может пропустить ни Кида, ни стража. Пламя метит узко (16×18), а вот
pop_chomp_redraw метит весь тайл со свесом — то есть пункт 1 чинит и часть
пунктов 3/4.
Связанные задачи: HEAL-WIDTH и G8 в perf_green_phase.md
— та же болезнь (полный тайл там, где хватает полосы), но на другом
материале.
6. Настоящая причина 190 260 тактов: перерисовка неизменной позы
Найдено при реализации P1, сверкой с animate_chomper (seg007:0448).
Функция оригинала заканчивается так:
if ((curr_modifier & 0x7F) < 6) {
redraw_at_trob();
}
То есть чомпер перерисовывается только пока фаза меньше 6 — пять кадров
из пятнадцати (POP_CHOMPER_SPEED = 15). Это не оптимизация оригинала, а
следствие таблицы поз: chomper_fram1 = {3,2,0,1,4,3,3}, и начиная с фазы 5
и до конца круга поза одна и та же — 3. Рисовать её десять кадров подряд
значит рисовать ровно ту же картинку.
Мы же метили тайл БЕЗУСЛОВНО, каждый кадр, пока trob жив — то есть платили
полный draw_tile плюс heal 32×64 за неизменную картинку в двух третях
кадров. А trob у чомпера живёт, пока Кид в том же РЯДУ (animate_chomper
снимает его только при фазе ≥ 6 и ушедшем Киде) — в 11/15 Кид стоит в (0,2),
чомпер в (0,3), ряд один.
Что сделано:
- пометка только при фазе < 6, и на фазе 5 — на ОБЕ страницы дабл-буфера (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу пометка догоняет в кадре фазы 6, где поза та же самая);
- пометка от факела (
set_redraw_anim_right) переведена на новый видPOP_RD_CHOMP_ANIM→pop_chomp_anim_draw: три блита графики чомпера поверх свежего пламени, без heal и без остальных слоёв — порт веткиredraw_frames_anim(seg008:0211); - приоритет полной перерисовки над anim в
pop_set_redraw— у оригинала это два независимых счётчика, иfullпобеждает.
Результат (замер, 552 кадра): полная перерисовка теперь в 40 % кадров, лёгкий возврат челюстей — в 60 %. Зелёная фаза: 291 888 в дорогом кадре против 183 414 в дешёвом.
| работа | зелёная | |
|---|---|---|
| до P1 | 768 684 | 294 510 |
| после P1, медиана | 657 882 | 183 420 |
| после P1, дорогой кадр (40 %) | 765 936 | 291 888 |
−110 802 на медиане при ожидании −160 000. Разница в том, что 40 %
кадров по-прежнему платят полную цену: там поза реально меняется, и это уже
не лишняя работа, а честная. Дальше её можно резать только раскладом
draw_tile на части (P7) или сужением heal (P8).
5. Журнал правок по этой сцене
| дата | правка | работа | синяя | зелёная | циан |
|---|---|---|---|---|---|
| 2026-08-19 | базовый замер (09f32ce) |
801 768 | 293 238 | 320 916 | 187 758 |
| 2026-08-19 | P5: гейты холостого хода в pop_loose_tick |
767 928 | 285 864 | 294 384 | 187 764 |
| −33 840 | −7 374 | −26 532 | +6 | ||
| 2026-08-19 | зонды для замера P2/P6 (временные) | 768 684 | 286 518 | 294 510 | 187 761 |
| 2026-08-19 | P1: чомпер — перерисовка только при фазе < 6 (медиана) | 657 882 | 286 503 | 183 420 | 187 761 |
| −110 802 | −15 | −111 090 | 0 |
Оснастка P2/P6 стоит 756 тактов на кадр — замеры до и после сопоставимы.
Циан не изменился (+6 тактов — шум), и это ожидаемо: loose_tick целиком
лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке —
скорее всего перераскладка кода банка 3 компилятором; проверять отдельно
не стали, знак верный.
7. ТЯЖЁЛАЯ позиция: Кид на шаг правее [замер 2026-08-19]
Поставлена пользователем: один осторожный шаг вправо (x = 106 вместо 99,
колонка та же). Спрайт Кида начинает пересекаться с тайлом (0,3), где
одновременно чомпер и пламя факела — и skip_mask перестаёт его
пропускать.
| фаза | лёгкая | тяжёлая | разница |
|---|---|---|---|
| синяя | 259 500 | 257 520 | −1 980 |
| зелёная (медиана) | 181 068 | 180 870 | −198 |
| циан | 187 761 | 319 842 | +132 081 |
| работа (медиана) | 628 542 | 758 358 | +129 816 |
| работа (максимум) | 744 384 | 876 612 |
Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %). Это уже не «стабильно медленно», а рывки: каждый четвёртый кадр длиннее соседних.
Разбор циана показывает, куда ушли 132 тысячи:
| участок | лёгкая | тяжёлая |
|---|---|---|
check_mirror |
3 198 | 3 198 |
mob_draw + guard_over_kid + skip_mask |
34 374 | 18 672 |
pop_char_draw(KID) |
204 | 54 738 |
соперник: char_draw + char_fore |
145 896 | 149 748 |
fore_needed + char_fore(KID) + борта |
4 356 | 93 486 |
То есть Кид из «пропущен за 204 такта» превращается в полноценного персонажа за ~144 000 — ровно столько же, сколько стоит страж.
Главный вывод замера: самая дорогая единичная статья кадра — это
fore-проход персонажа. 62 778 у стража и ~89 000 у Кида, вместе около
152 000, то есть 20 % работы кадра. У Кида он дороже потому, что в его
футпринте лежит чомпер, а у чомпера есть собственный передний слой
(POP_CHOMP_FRAM_FOR), который перерисовывается поверх персонажа каждый
кадр.
8. После P15 (точная метка «фон трогали»)
| фаза | лёгкая до | лёгкая после | тяжёлая до | тяжёлая после |
|---|---|---|---|---|
| синяя | 259 050 | 223 902 | 257 520 | 245 808 |
| зелёная | 181 494 | 181 494 | 180 870 | 181 761 |
| циан | 194 262 | 59 406 | 319 842 | 192 090 |
| работа | 628 542 | 464 796 | 758 358 | 617 487 |
В лёгкой позиции не рисуется НИ ОДИН персонаж (циан 59 406 — это уже только
check_mirror, проверки и передний слой по пометкам). В тяжёлой рисуется
один Кид: он действительно стоит под пламенем, а страж — нет.
Пятирастровые кадры в тяжёлой позиции исчезли (было 27 %), период стал ровно 4.
Полная очередь оптимизаций с оценками — perf_registry.md.
Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603
блита) и модель зелёной фазы этой сцены.