31b82661eb
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
239 lines
16 KiB
Markdown
239 lines
16 KiB
Markdown
# Сцена и замер: факел + чомпер + страж, уровень 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`, база модуля `sprpop.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`), который перерисовывается поверх персонажа каждый
|
||
кадр.
|
||
|
||
## 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`](perf_registry.md).**
|
||
Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603
|
||
блита) и модель зелёной фазы этой сцены.
|