Files
Sprinter-SDCC/applications/PoP/docs/perf_l11_room15.md
T
snark13 d3049693b8 Реестр: сводка и статусы обновлены; замер тяжёлой позиции
Пользователь передвинул Кида на один осторожный шаг вправо (x = 106
вместо 99, колонка та же) — его спрайт начал пересекаться с тайлом (0,3),
где одновременно чомпер и пламя факела.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:30:04 +03:00

15 KiB
Raw Blame History

Сцена и замер: факел + чомпер + страж, уровень 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: механизм, который правдоподобно объясняет цифру, ещё не доказан цифрой.

Цепочка:

  1. TORCH_ANIM_DIV = 1 — факел меняет кадр пламени КАЖДЫЙ логический кадр;
  2. пламя запекается в фон (pop_torch_draw, GFX_BANK_NORMAL), а канвас пламени 16×18 лежит в ячейке правого соседа (seg008:560) — то есть поверх чомпера;
  3. поэтому pop_process_trobs метит соседа: if (trob_rcode[i] == TILE_CHOMP) pop_set_redraw(tp + 1, POP_RD_CHOMP, 1) (порт set_redraw_anim_right);
  4. pop_chomp_redraw отвечает на пометку heal 32×64 + полный draw_tile.

Расхождение с оригиналом именно в шаге 4. set_redraw_anim_right метит слой anim, и оригинал возвращает только его (draw_tile_anim_toprightdraw_tile_anim_rightdraw_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), ряд один.

Что сделано:

  1. пометка только при фазе < 6, и на фазе 5 — на ОБЕ страницы дабл-буфера (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу пометка догоняет в кадре фазы 6, где поза та же самая);
  2. пометка от факела (set_redraw_anim_right) переведена на новый вид POP_RD_CHOMP_ANIMpop_chomp_anim_draw: три блита графики чомпера поверх свежего пламени, без heal и без остальных слоёв — порт ветки redraw_frames_anim (seg008:0211);
  3. приоритет полной перерисовки над 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), который перерисовывается поверх персонажа каждый кадр.

Полная очередь оптимизаций с оценками — perf_registry.md. Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603 блита) и модель зелёной фазы этой сцены.