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

223 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сцена и замер: факел + чомпер + страж, уровень 11 комната 15
Вторая целевая сцена для оптимизации (первая — [`perf_l13_room23.md`](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_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`](perf_green_phase.md)
— та же болезнь (полный тайл там, где хватает полосы), но на другом
материале.
## 6. Настоящая причина 190 260 тактов: перерисовка неизменной позы
Найдено при реализации P1, сверкой с `animate_chomper` (seg007:0448).
Функция оригинала заканчивается так:
```c
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_ANIM` → `pop_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`](perf_registry.md).**
Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603
блита) и модель зелёной фазы этой сцены.