Files
Sprinter-SDCC/applications/PoP/docs/perf_registry.md
T
snark13 a993cb3b62 Реестр: P4 частично, эффект много меньше ожидаемого
Каталог атласа теперь читается из W0 вместо переключения W3 — минус 408
на кадре при ожидании минус 5 400.  Цена блита 16 107 -> 16 005.

Причина записана: 672 такта atlas_image — это почти целиком вызов
функции и арифметика idx*8, а не переключение окна; после правки работа
переехала в статью «каталог + шапка + клип» (2 694), а сам gfx_w0_map
стоит всего 324.

Отсюда понижена оценка непробованной части P4 (один map на группу
блитов): потолок ~2 600 за кадр, а не 10 000.

Бюджет: лёгкая 438 570, тяжёлая 602 574.  До цели 8 570 и 172 574.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:41:14 +03:00

725 lines
51 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.
# Реестр оптимизаций: всё отложенное, в одном списке
Собрано 2026-08-19 из [`perf_green_phase.md`](perf_green_phase.md) (G1-G9),
[`perf_cyan_phase.md`](perf_cyan_phase.md) (C1-C7),
[`perf_backlog.md`](perf_backlog.md) (позиции 1-7),
[`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) (HEAL-WIDTH) и из
свежего разбора сцены [`perf_l11_room15.md`](perf_l11_room15.md).
**База для процентов — работа кадра в 11/15: 801 768 тактов** (замер
`09f32ce`). Где эффект относится к другой сцене, это сказано явно.
Оценки помечены: **[замер]** — измерено; **[модель]** — посчитано по
измеренным составляющим; **[гипотеза]** — не мерено, нужен прогон.
---
## 1. Цена одного блита фона — разложена [замер 2026-08-19]
Метод: брейкпоинты на резидентных адресах внутри `pop_blit_b` (0x5C50),
`temp0` на входе, разница `totalcycles` на каждом вызове. 1603 блита.
| этап | такты | постоянство |
|---|---:|---|
| пролог + аргументы + грубый отсев | **810** | ровно, всегда |
| `atlas_image` | **672** | ровно, всегда |
| `gfx_w0_map` + чтение шапки ленты + арифметика клипа | **2 400** | ровно, всегда |
| ядро блита (пиксели) | 4 380 … 26 592 | по размеру кадра |
| `pop_cd_touch` | **2 069** (пакетный путь) / 4 115 (настоящая пометка) | почти ровно |
| `gfx_w0_unmap` + эпилог | **175** | ровно, всегда |
| **весь блит** | 10 458 … 32 718, медиана **16 674** | |
**Фиксированная накладная = 6 126 тактов на любой блит, хоть 8×8.**
У самого дешёвого блита (10 458) это **59 % цены**; у пламени факела 16×18
пиксели тянут ~1 700 из ~14 000, то есть **12 %**.
Это и есть ответ на вопрос «почему маленький блит стоит 14 000». Причины
ровно те, о которых спрашивал пользователь:
- **810 на пролог** — `call ___sdcc_enter_ix`, IX-фрейм и шесть чтений
`N(ix)`: третий и четвёртый аргументы (`int x`, `int ybottom`) идут
СТЕКОМ, каждое обращение 19 тактов Z80;
- **2 069 на `pop_cd_touch`** даже по пакетному пути, где вся работа — четыре
сравнения. Сигнатура `(int x, int y, int w, int h)` = 8 байт аргументов,
два из них через стек; `w`/`h` никогда не больше 64, `y` не больше 255,
то есть три из четырёх могли быть `uint8_t`;
- **2 400 на map + шапку** — `gfx_w0_map` (1 086) + `unmap` (264, платится в
конце) + ~1 000 на чтение четырёх байт заголовка и арифметику;
- **672 на `atlas_image`** — маппинг W3, чтение записи каталога, возврат W3;
размеры при этом читаются и выбрасываются (см. §3, C6).
**Блитов за кадр в 11/15: 8** [замер] — все в зелёной фазе (6 на `draw_tile`
чомпера, 2 на пламя факелов). Синяя и циан через `pop_blit_b` не ходят
вовсе: heal и персонажи идут своими путями. Значит фиксированные накладные
блита стоят сцене **8 × 6 126 = 49 000 тактов/кадр (6,1 % работы)**.
---
## 2. Модель зелёной фазы 11/15 [модель, сходится с 320 916 замера]
| статья | такты | доля фазы |
|---|---:|---:|
| 8 блитов фона (из них 6 126×8 = 49 000 накладных) | 133 400 | 41 % |
| диспетчер `draw_tile` (один тайл чомпера) | 56 200 | 18 % |
| цикл `pop_process_trobs` без блитов пламени | 59 100 | 18 % |
| `pop_loose_tick` (плит в комнате НЕТ) | 28 872 | 9 % |
| heal 32×64 в `pop_chomp_redraw` | 34 000 | 11 % |
| шов / ворота соседа | 9 336 | 3 % |
Больше половины фазы — не пиксели, а обвязка вокруг них.
---
## 3. Список приёмов, отсортированный по эффекту
### P1. Чомпер: перерисовка неизменной позы ✅ СДЕЛАНО 2026-08-19 — 110 802
**Диагноз, с которым позиция заводилась, оказался неполным.** Я приписал
190 260 тактов пометке от факела; зонд `pop_dbg_kind` показал, что все 312
перерисовок прогона — вид `POP_RD_CHOMP` (полная), а пометку соседа она
просто перебивала. Настоящая причина нашлась сверкой с `animate_chomper`
(seg007:0448): оригинал перерисовывает чомпер **только при фазе < 6**, пять
кадров из пятнадцати, потому что с фазы 5 поза не меняется
(`chomper_fram1 = {3,2,0,1,4,3,3}`). Мы метили тайл каждый кадр.
Сделано три вещи: условие фазы (с пометкой обеих страниц на фазе 5), новый
вид `POP_RD_CHOMP_ANIM``pop_chomp_anim_draw` (три блита поверх огня, порт
ветки `redraw_frames_anim`) и приоритет полной перерисовки над anim в
`pop_set_redraw`. Обе половины работают: замер даёт 40 % полных
перерисовок и 60 % лёгких.
Работа 768 684 → **657 882** (медиана), зелёная 294 510 → **183 420**.
В 40 % кадров цена осталась прежней — там поза действительно меняется, и это
уже честная работа; резать её дальше только через P7 (раскол `draw_tile`)
или P8 (ширина heal).
Полный разбор — [`perf_l11_room15.md`](perf_l11_room15.md) §6.
<details><summary>Исходная (неполная) постановка</summary>
Разбор в [`perf_l11_room15.md`](perf_l11_room15.md) §3. Сейчас пометка от
факела обрабатывается как `heal 32×64 + полный draw_tile` (190 260 тактов на
единственный перерисованный тайл кадра); оригинал в этом случае рисует
ТОЛЬКО `draw_tile_anim` — графику чомпера поверх свежего пламени.
Останется 1-2 блита челюстей ≈ 20 000-33 000. **Риск низкий**: это
сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку
«фон трогали» вокруг тайла чомпера — см. P3.
</details>
### P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась
Замер зондами внутрь обеих половин синей (286 518 тактов):
| участок | такты | доля работы |
|---|---:|---:|
| ввод + читы | 14 154 | 1,8 % |
| три спецсобытия уровней (skel / mouse / killed_shadow) | **1 650** | 0,2 % |
| `pop_frame_timers` + **луч видимости стража** | **37 032** | 4,8 % |
| `pop_ctrl_tick` | 18 648 | 2,4 % |
| `skip_mask` + **heal двух Char** | **67 734** | 8,8 % |
| `mirror_heal` + `fore_heal` | 2 040 | 0,3 % |
| `kid_tick` (play_seq) | 10 098 | 1,3 % |
| **`pop_phys_tick`** (физика Кида) | **61 266** | 8,0 % |
| `pop_guard_tick` (логика стража) | 19 932 | 2,6 % |
| **`pop_guard_phys_tick`** (физика стража) | **43 980** | 5,7 % |
| боёвка (`sword_hurting` / `sword_hurt` / `delta_hp`) | 9 558 | 1,2 % |
| `guard_fallout` + уход из комнаты | 366 | — |
**Что оказалось не так, как ждали.** Я предполагал, что дорогие тут
банковые трамплины на спецсобытиях (по аналогии с `pop_clip_char_top`,
8 892 такта за трамплин ради одной проверки). Замер это отверг: три
спецсобытия уровней вместе стоят **1 650** — они гейтятся внутри и на
уровне 11 честно выходят сразу.
**Настоящие статьи — три:**
1. **Физика двух Char — 105 246** (61 266 + 43 980), при том что оба
персонажа СТОЯТ и кадр позы не меняется. Это 13,7 % работы кадра и
самая крупная статья синей. Нужен ещё один уровень разбора — внутрь
`pop_phys_tick` (позиция **P2a**, отдельным заходом).
2. **Луч видимости стража — до 37 032** (вместе с `pop_frame_timers`, но тот
заведомо копеечный: три счётчика). Считается КАЖДЫЙ кадр, хотя ни Кид,
ни страж не сдвинулись. Кандидат на гейт «пересчитывать только при
смене позиции или комнаты любого из двоих» — позиция **P2b**,
ожидание −30 000, риск низкий.
3. **heal двух Char — 67 734.** Отдельной правки не требует: он платится
ровно потому, что `skip_mask` никого не пропускает, и уйдёт вместе с
P1/P3.
Новое, найдено 2026-08-19.
### P15. Точность метки «фон трогали» ✅ СДЕЛАНО 2026-08-19 — 163 746 / 140 871
Постановка пользователя: не перерисовывать стража, пока он не двигается.
**Две правки, и вторая оказалась решающей:**
1. **метка**: вместо «маска колонок × три ряда по 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 —
и прямоугольник «спрайт + клинок» цеплял метку углом.
**Без второй правки первая дала почти ноль** (632 676 против 628 542 до
неё) — это стоит помнить: точность структуры бесполезна, пока запрос к ней
остаётся грубым.
| | лёгкая позиция | тяжёлая позиция |
|---|---:|---:|
| синяя | 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** |
В тяжёлой позиции вдобавок исчезли пятирастровые кадры (было 27 %).
Проверено в MAME: статика чистая, динамика (пробежка, бой, переход в
соседнюю комнату) без хвостов и просвечивания; хост-тесты зелёные.
Побочно исправлены два собственных дефекта первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0 (после
`pop_cd_clear(0)` метка второй переставала расти), и отсутствовала явная
инициализация — пустая колонка обозначается `ymin = 255`, а нули от crt0
читались бы как «затронута строка 0».
### P16. Цианные проверки ✅ СДЕЛАНО 2026-08-19 — 25 818
Раскладка остатка цианной фазы (59 406) показала, что 47 883 из них — три
вызова, а не отрисовка:
| вызов | было | стало |
|---|---:|---:|
| `pop_loose_mob_draw` | 978 | 978 (гейт `mobs_live` работает) |
| `guard_over_kid` | 14 424 | **0** |
| `pop_char_skip_mask` | 28 605 | ~21 000 |
Три правки:
1. **`cd_sig_same`** — сравнение снимка БЕЗ построения структуры.
`cd_sig_make` записывал тринадцать полей в стековый кадр (через
`-n(ix)`), и лишь потом шёл побайтовый цикл; теперь сравнение идёт прямо
с источником и выходит на первом расхождении. **8 016**;
2. **`guard_over_kid` по условию** — вопрос «кто поверх кого» не имеет
смысла, когда не рисуется никто. Вызов перенесён ПОСЛЕ `skip_mask` и
идёт только при `skip != 3`. **14 118**;
3. **`pop_cd_hit_slot`** — проверка «задет ли слот» брала пять аргументов,
три из них стеком (45 % тактов на IX). Теперь координаты берутся из
`pop_cd`, а сравнение вынесено в `hit_rect` с file-scope аргументами.
**3 684**.
**Отрицательный результат внутри третьей правки** (не повторять): первая
версия была обёрткой, которая внутри всё равно звала `pop_cd_hit` с пятью
аргументами — стало ХУЖЕ (1799 тактов Z80 вместо 1318). Снимать аргументы
со стека надо у того, кто их читает, а не этажом выше.
### Отрицательные результаты 2026-08-19 — НЕ ПОВТОРЯТЬ
Три попытки подряд сделали ХУЖЕ. Общая ошибка в двух из них — я оценивал
правку по СУММЕ ТАКТОВ ИНСТРУКЦИЙ в листинге, а не по реально исполняемому
пути.
**1. `cd_sig_same` блоком вместо тринадцати сравнений.** Снимок был
переложен так, чтобы сравнивать непрерывные 10 байт начала `pop_char_t`
циклом `do { if (*a++ != *b++) return 0; } while (--i)`. По листингу
функция стала короче (1939 → 1290 тактов), а на машине **стало хуже:
438 978 → 450 426 (+11 448)**.
Причина: сумма по листингу считает каждую инструкцию ОДИН раз, а тело
цикла исполняется ДЕСЯТЬ раз. Тринадцать линейных сравнений выполняются по
разу каждое и выходят раньше на первом же расхождении. **Урок: короткий
листинг ≠ быстрый код; цикл надо разворачивать в уме.**
**2. `cd_touch_pb` — пометка «для блита» из file-scope.** `pop_cd_touch`
зовётся из `pop_blit_b` с четырьмя аргументами, хотя тот держит те же
значения в `pb_x`/`pb_top`/`pb_w`/`pb_h`. Специализированный вход без
аргументов дал **438 978 → 442 242 (+3 264)**.
Причина: в зелёной фазе блиты идут ПАКЕТНЫМ путём (`draw_tile` открывает
`pop_cd_batch`), а там нужны все четыре значения сразу — и в регистрах
(`x`, `y` приходят в HL/DE) они дешевле, чем чтение из статиков.
**Снятие аргументов со стека помогает не всегда: если значение и так живёт
в регистре, статик его туда ещё и загружать заставит.**
**3. Обёртка `pop_cd_hit_slot` поверх `pop_cd_hit`** (описана в P16):
внутри всё равно звала функцию с пятью аргументами и добавила свои — стало
1799 тактов вместо 1318. Помогло только когда сравнение переехало внутрь.
### P14. Fore-проход персонажа — от 4 122 до 117 570 [замеры 2026-08-19]
**Самая НЕСТАБИЛЬНАЯ статья кадра.** Замеры на одной и той же сцене:
| ситуация | fore-проход |
|---|---:|
| персонаж пропущен (`skip`) | 4 122 |
| стоящий страж | 62 778 |
| живой Кид у чомпера | ~89 000 |
| **труп Кида в челюстях** | **117 570** |
Растёт от двух вещей: ширины футпринта (широкий кадр смерти, вынутый меч
добавляет колонку) и числа тайлов с передним слоем внутри футпринта (здесь
чомпер со своими зубьями). Отсюда практический вывод: **в бою проход будет
ближе к сотне тысяч, чем к шестидесяти** — кадры выпадов и ударов широкие.
Замер трупа сделан по просьбе пользователя. Сама по себе эта ситуация не
игровая («когда Кид — труп, игры нет»), но именно она показала верхнюю
границу цены.
**РАЗБОР 2026-08-19: P14 сводится к P4.** Fore-проход Кида в тяжёлой
позиции (89 268) разложен зондами:
| участок | такты |
|---|---:|
| вход + `pop_fore_set_clip` + `char_footprint` | 10 872 |
| арифметика границ окна | 3 786 |
| шов ворот + overlay-цикл | 3 294 |
| **цикл `fore_tile` по тайлам** | **67 854** (76 %) |
| `pop_gate_over_char` + хвост | 3 462 |
А счётчик показал, что цикл обходит **всего 4 тайла**, и 3 из них реально
рисуют (`FORE_ANY != 0`). То есть 67 854 — это НЕ перебор лишних тайлов
(их четыре) и не проверки, а **цена самих блитов переднего слоя**: около
четырёх блитов по ~16 000, из которых 6 765 на каждом — фиксированная
накладная (см. §1).
**Отсюда вывод для плана:** отдельной «оптимизации fore-прохода» почти нет.
Срезать там можно ровно три вещи, и только первая крупная:
1. **цену блита (P4)** — 4 блита × 6 765 накладных = ~27 000 из 67 854;
2. `char_footprint` из физики (**P10**) — часть от 10 872;
3. слияние двух трамплинов в банк 2 — ~4 000.
Иначе говоря, **P4 ускоряет и зелёную фазу (8 блитов), и fore-проход
(4 блита), то есть работает и в статике, и в динамике** — в отличие от
P14, который я считал самостоятельной позицией.
`pop_char_fore` = два трамплина в банк 2 (`pop_fore_set_clip` +
`pop_fore_over_char`) плюс обход тайлов футпринта, в каждом `fore_tile`.
У Кида дороже, чем у стража, потому что в его футпринте лежит чомпер, а у
чомпера есть собственный передний слой (`POP_CHOMP_FRAM_FOR`), который
перерисовывается поверх персонажа каждый кадр.
Что можно пробовать, по возрастанию радикальности:
1. слить два трамплина в один вызов (мелочь, ~4 000);
2. **P10** — брать футпринт из физики, а не считать заново (−11 574 на
проход, то есть до −23 000 на двоих);
3. гейт по сигнатуре: пропускать проход, если не изменились ни кадр
персонажа, ни тайлы его футпринта. **Это расхождение с оригиналом**
он рисует foretable безусловно;
4. **P13** — objtable и отложенные таблицы: у оригинала «посетить тайл»
стоит копейки именно потому, что таблицы только копят записи.
### P3. Персонажи будятся каждый кадр ✅ ЧАСТИЧНО СБЫЛОСЬ
**В лёгкой позиции — да:** после P1 `pop_char_draw(KID)` стоит 204 такта,
метка от чомпера до Кида больше не дотягивается.
**В тяжёлой позиции — нет:** стоит Киду шагнуть на 7 пикселей вправо, и его
спрайт пересекается с тайлом чомпера и пламени, `skip_mask` перестаёт
пропускать, и он снова стоит ~144 000 (54 738 draw + ~89 000 fore). То
есть выигрыш P3 держится только пока персонаж не подошёл к анимированному
тайлу — а в игре он к нему подходит постоянно.
Исходная оценка (−70 000) была:
heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то,
что `pop_char_skip_mask` не может пропустить ни Кида, ни стража: фон трогают
каждый кадр.
- **Кида** спасает P1: широкая пометка вокруг чомпера исчезнет;
- **стража спасти нельзя** — пламя правого факела (0,7) рисуется в ячейке
(0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже
перерисовывает.
### P4. Накладные блита — ЧАСТИЧНО, эффект много меньше ожидаемого
**Сделано 2026-08-19: каталог атласа читается из W0** (`atlas_image_w0` в
libbgi) вместо переключения W3 в `atlas_image`. **408 на кадре.**
Ожидание было −5 400 (672 такта × 8 блитов зелёной фазы), и оно НЕ
оправдалось: цена блита 16 107 → 16 005, то есть −102. Причина в том, что
эти 672 — почти целиком вызов функции и арифметика `idx * 8`, а не само
переключение окна. Замер после правки показывает, что работа просто
переехала между статьями:
| этап | до | после |
|---|---:|---:|
| пролог + отсев | 810 | 762 |
| `gfx_w0_map` | (в составе 2 400) | **324** |
| каталог + шапка ленты + клип | | **2 694** |
| ядро | 8 508 | 8 508 |
| `cd_touch` + `unmap` + эпилог | 2 883 | 2 883 |
| **фиксированная накладная** | **6 765** | **6 663** |
Правка оставлена: не вредит, убирает лишнее переключение W3 и делает
контракт честнее (страница мапится один раз). Но как способ снять
накладные она не работает.
**Что осталось непробованным** (и во что я теперь верю меньше): один
`gfx_w0_map` на ГРУППУ блитов — судя по замеру, сам map стоит 324, так что
потолок этой правки ~2 600 за кадр, а не 10 000, как считалось.
<details><summary>Исходная постановка (модель 28 000)</summary>
| правка | на блит | источник |
|---|---:|---|
| `pop_cd_touch`: `uint8_t` вместо `int` для `y`/`w`/`h`, ранний выход пакетного пути | ~−1 300 | новое |
| один `gfx_w0_map`/`unmap` на ГРУППУ блитов | ~1 350 | C5 / backlog §3 |
| размеры ленты из каталога, без `atlas_image` и чтения шапки | ~670 | C6 / backlog §2 |
| `pop_blit_b`: аргументы в 8 бит, где хватает | ~−400 | новое |
Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из
6 126 фиксированных.
</details>
### P5. `pop_loose_tick` при пустой комнате — 28 872 → 2 760 ✅ СДЕЛАНО 2026-08-19
**Получено −26 112 внутри функции, −33 840 на кадре** (замер до/после в
11/15). Оценка была 28 000.
Раскладка холостого хода (замер зондами m9..m12) и что с ней стало:
| участок | было | стало |
|---|---:|---:|
| два цикла по тайлам (30 + 10 позиций) | 9 852 | **132** |
| `pop_loose_mob_tick` (обход 14 слотов) | 12 090 | **996** |
| `check_loose_fall_on_kid` (трамплин + обход) | 5 868 | **546** |
| вход + хвост | 1 062 | 1 086 |
| **итого** | **28 872** | **2 760** |
Сделано двумя гейтами:
- `loose_any` (статик `pop_map.c`) — «идёт ли анимация плит». Ставится в
пяти местах записи ненулевой фазы, снимается САМИМ циклом по факту
прохода, где не осталось ни одной живой фазы;
- `pop_mob_busy` (резидент `pop_state.c`) — «занят ли хоть один слот
падающего куска» (`active` или дочистка `clean`). Ставит `mob_alloc`,
снимает обход по факту пустой таблицы. В резиденте, а не в `pop_room.c`,
потому что читает его `pop_map` из банка 3.
**Важно про границу:** гейт отвечает не на «есть ли в комнате плиты», а на
«идёт ли анимация». У лежащей плиты-потолка фаза 0, и крутить нечего —
вопрос пользователя 2026-08-19. Асимметрия намеренная: ложная единица
стоит одного холостого прохода, ложный ноль — застывшей навсегда плиты,
поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту.
Покрытие: `phys_loose_floor_breaks` (взвод от шага и сотрясения) и новый
`phys_loose_gate_survives_room_change` — на пятое место взвода
(фаза восстановлена входом в комнату), которое не покрывал никто.
Мутационная проверка: со снятым взводом тест падает.
### P6. `pop_process_trobs` разложен [ЗАМЕР 2026-08-19] — 89 784
| участок | такты |
|---|---:|
| вход + префетч кодов тайлов (маппинг окна 0) | **11 058** |
| цикл: два `pop_torch_draw` | ~36 000 |
| цикл: обход самих trob'ов | ~43 000 |
Цена одного `pop_pot_b` (пламя факела) измерена отдельно, брейкпоинтами на
резидентных адресах: **17 346 тактов**, и это ЕДИНСТВЕННАЯ группа в
распределении — то есть `pop_pot_b` в кадре зовут только два факела. При
канвасе пламени 16×18 сами пиксели там 1 716, то есть **10 % цены**; всё
остальное — накладные (см. §1) плюс ~6 900 сверх `pop_blit_b` на самом
`pop_pot_b`.
Направления:
- **P6a**: `pop_trob_modif(room)` зовётся банковым вызовом на КАЖДЫЙ trob
внутри цикла, хотя комната у них одна и та же — вынести наружу;
- **P6b**: префетч 11 058 маппит окно 0 каждый кадр, а коды тайлов trob'ов
меняются редко — кэшировать с инвалидацией по смене тайла/комнаты;
- **P6c**: цена факела — это цена блита, то есть позиция P4.
Новое, найдено 2026-08-19.
### P7. G5. Раскол `draw_tile` на узкие части [оценка дока: −50 000 … 60 000]
Диспетчер + контекст оплачиваются целиком всегда; у оригинала это девять
независимых функций. В 11/15 это те самые 56 200 на один тайл — но если
сделан P1, `draw_tile` чомпера вообще не вызывается, и здесь эффект пропадёт.
Ценность приёма — в ДРУГИХ сценах (13/23, любая комната с плитами).
**Риск средний**: в `draw_tile` собрано много инвариантов (BUG-LOOSE-3,
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным заходом с прогоном
всех уровней.
### P8. HEAL-WIDTH — ширина heal'ов по фактическому следу [оценка: 5-6 % цены heal'ов]
ОБЯЗАТЕЛЬНАЯ по решению пользователя (2026-08-18). След плиты 58 px в
подземелье / 57 во дворце против используемых 60 и 64.
Постановка — `TASKS_OPEN.md`, якорь `heal-width`.
### P9. G8 — пометку СОСЕДА ставить узкой полосой (28 px), а не тайлом [гипотеза]
Парная к HEAL-WIDTH. Относится к сценам с падающими плитами (13/23), в 11/15
не играет. Разбор — `perf_green_phase.md` §G8, там же три условия, из-за
которых это не «просто уменьшить число».
### P10. Футпринт персонажа — из физики, а не считать заново [замер: −11 574 на fore-проход]
`backlog` §1. С двумя персонажами — ~23 000 за кадр. Мешает то, что физика
(банк 3) держит `char_col_left/right` в статиках, а слой фона — банк 2.
**Риск средний**: окно fore-клипа заводилось под клинок и брызги.
### P11. Мелочи с известной ценой [замер, `backlog` §7]
| что | цена | где |
|---|---:|---|
| `pop_clip_char_top` — трамплин банк 4 → банк 3 ради одной проверки | 8 892 | `pop_cdraw.c` |
| `cd_sig_make` + возврат из `pop_char_draw` | 7 944 | `pop_cdraw.c` |
| `pop_loadkid` + расчёт координат кадра | 7 410 | `pop_cdraw.c` |
| `obj_x * 8 / 7` — последнее `__divsint` в горячем пути | ~2 400 | `pop_char_draw` |
### P12. G9 — снять временную оснастку [замер: −6 000]
`pop_dbg_b1..b6` в `pop_blit_b` (~400 на блит), `pop_dbg_kind`/`m16`,
`pop_dbg_m5..m15`, счётчик `rd_cnt` в `pop_redraw_needed`.
**Только ПОСЛЕ окончания оптимизации** — без них не мерить.
### P13. Крупные рефакторинги — брать, только если понадобится ещё запас
**Чем P13 НЕ является (вопрос пользователя 2026-08-19).** Это не «рисовать
комнату заново каждый кадр в скрытый буфер». Такой вариант исключён
арифметикой: 30 тайлов по 5-6 спрайтов при цене блита 16 674 (и 179 914 за
полную запечку одного тайла) дают порядка **3 000 000 тактов — семь
растровых кадров**. Оригинал так тоже не делает: у него та же
инкрементальная схема с пометками (`redraw_frames_full` / `_anim` /
`_fore`), перерисовываются только помеченные тайлы.
Разница не в объёме отрисовки, а в цене ПОСЕЩЕНИЯ тайла: у нас
`fore_tile(r, c)` сразу блитит (со всеми 6 126 фиксированных накладных), а
у оригинала `add_backtable`/`add_midtable`/`add_foretable` только кладут
запись в массив, и рисует один `draw_table()` в конце. Плюс у него ОДИН
обход тайлов за кадр против наших трёх.
- **C7 / backlog §5-6: objtable + отложенные таблицы back/mid/fore.** У
оригинала «посетить тайл» стоит копейки, потому что таблицы только копят
записи, а рисует один `draw_table()` в конце. У нас блит идёт сразу из
обхода, и fore-проход отдельный НА КАЖДОГО персонажа.
- **backlog §4: единый проход по тайлам вместо трёх** (`pop_redraw_needed`,
`pop_process_trobs`, `pop_fore_over_char`) и семь счётчиков причин
перерисовки вместо одного `kind`.
- **G6: меньше блитов в `RD_FLOOR`**, **G7: `mob_tick_one` в file-scope**.
---
## 4. ПЛАН РАБОТ — состояние между сессиями
Рабочий чеклист. Правило: одна позиция = один заход = один коммит с замером
до/после на сцене 11/15. Замер обязателен даже когда «очевидно» — из семи
закрытых позиций ТРИ дали не то, что ожидалось (P1 — вдвое меньше, P2a —
почти ничего, таблицы порогов — регресс).
### Закрыто
| # | что | факт |
|---|---|---|
| P15 | точность метки «фон трогали» + раздельная проверка клинка | **163 746** лёгкая / **140 871** тяжёлая |
| P16 | цианные проверки: снимок без структуры, `guard_over_kid` по условию, `hit_slot` без пяти аргументов | **25 818** |
| P5 | `loose_tick`: гейты холостого хода | **33 840** (ждали 28 000) |
| P1 | чомпер: перерисовка только при фазе < 6 | **110 802** (ждали 160 000) |
| P2b | луч видимости: колонки + один банковый вызов | **26 448** (ждали 30 000) |
| P2a | `coll_scan` в 8 бит + снят с IX | **−2 892** (крупной статьи в физике нет) |
| P3 | Кид перестал будиться каждый кадр | сбылось само после P1 — но только в ЛЁГКОЙ позиции |
| P2/P6 | замеры синей фазы и `process_trobs` | гипотеза «трамплины на спецсобытиях» отвергнута |
| — | замер цианной фазы | крупного лишнего в отрисовке персонажа нет |
### Осталось, по убыванию ожидаемого эффекта
| # | что | ожидание | риск | комментарий |
|---|---|---:|---|---|
| P4 | накладные блита — 6 663 на КАЖДЫЙ блит | частично сделано: **−408** | низкий | из четырёх правок сработала слабо; разбор ниже |
| P14 | fore-проход персонажа | сводится к P4 + P10 | — | разбор ниже: цикл обходит всего 4 тайла |
| P11 | мелочи с известной ценой | −26 000 | низкий | `clip_char_top` 8 658 подтверждён замером |
| P10 | футпринт персонажа из физики | −23 000 | средний | часть P14 |
| P6a/P6b | `trob_modif` из цикла, кэш префетча | −20 000 | низкий | тот же паттерн трамплина в цикле |
| P7 | раскол `draw_tile` (G5) | 50 000 в 13/23 | средний | в 11/15 не играет |
| P8/P9 | HEAL-WIDTH + G8 | 5-6 % цены heal'ов | низкий/средний | **обязательная** по решению пользователя; для сцен с плитами |
| P13 | objtable + отложенные таблицы, единый проход по тайлам | не оценено | очень высокий | большой рефакторинг слоя фона |
| P12 | снять оснастку | −6 000 | нулевой | **последней**: без неё не мерить |
### Текущее состояние бюджета
| | работа | синяя | зелёная | циан | период |
|---|---:|---:|---:|---:|---:|
| до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра |
| после P5 | 767 928 | 285 864 | 294 384 | 187 764 | 4 |
| после P1 (медиана) | 657 882 | 286 503 | 183 420 | 187 761 | 4 |
| после P2a | 654 990 | 283 215 | 183 798 | 187 812 | 4 |
| **после P2b (лёгкая позиция)** | **628 542** | 259 500 | 181 068 | 187 761 | 4 |
| ТЯЖЁЛАЯ позиция (Кид на шаг правее) | 758 358 | 257 520 | 180 870 | 319 842 | **4 и 5** |
| **после P15, лёгкая** | **464 796** | 223 902 | 181 494 | **59 406** | 4 |
| после P15, тяжёлая | 617 487 | 245 808 | 181 761 | 192 090 | **4 везде** |
| после P16, лёгкая | 438 978 | 218 052 | 181 494 | 39 438 | 4 |
| **после P4, лёгкая** | **438 570** | 217 824 | 181 086 | 39 210 | 4 |
| **после P4, тяжёлая** | **602 574** | 242 082 | 181 347 | 181 404 | 4 |
Итог восьми позиций: **801 768 → 464 796 в лёгкой позиции (−42 %)** и
**758 358 → 617 487 в тяжёлой (19 %)**. Отдельно важно: в тяжёлой позиции
исчезли пятирастровые кадры (было 27 %), период стал ровно 4 — рывки ушли.
### Достижима ли цель — арифметика на 2026-08-19
Цель: работа ≤ 430 000, тогда период станет 3 растра (хвост кадра — три
`gfx_wait_vsync`).
- в ЛЁГКОЙ позиции снять надо **8 570**;
- в ТЯЖЁЛОЙ — **172 574**.
**Лёгких путей больше не осталось.** За 2026-08-19 отвергнуто ЧЕТЫРЕ
правки подряд (три с регрессом, одна почти без эффекта), и все они целили
в накладные проверок и блита. Фиксированная часть блита 6 663 держится
ядром `gfx_w0_map`/`cd_touch`/чтения шапки, а не «лишними» вызовами.
Всё оставшееся в списке, кроме P13, даёт по оценкам **порядка 100 000** — и
это оптимистично. **Арифметика не сходится:** сцена с двумя персонажами,
чомпером и двумя факелами в три растра не укладывается без одного из трёх
решений:
1. **P13** — переход на objtable и отложенные таблицы, как в оригинале
(единственный резерв нужного размера, но это переписывание слоя фона);
2. **осознанное расхождение с оригиналом** — например, не перерисовывать
передний слой персонажа, пока не изменились ни персонаж, ни тайлы под
ним (гейт по сигнатуре футпринта);
3. **принять 4 растра** как рабочий режим для сцен такой плотности и
выравнивать период, чтобы не было рывков 4/5.
Решение за пользователем — это выбор между точностью порта и скоростью.
### Как воспроизвести сцену (важно для следующей сессии)
Сборка стартует прямо в ней: `make` (дефолты `LEVEL=11 ROOM=15 POS=2`) →
`make hdd`**полный рестарт MAME** (`chdman -f` даёт новый inode, memory
`mame_hdd_rebuild_restart`) → в DSS набрать `d:` и `roomtest`. Кид встаёт в
(0,2) лицом к чомперу, справа факел и страж — та самая сцена замеров.
Штатный старт уровня возвращается через `make ROOM=`.
Проверка, что программа ЖИВА, обязательна перед любым чтением памяти:
`cur_room` (0x97AA) должен лежать в 1..24 — на этом уже был сорван один
замер (прочитаны два случайных байта остановленной машины).
### Метод замера
Зонды — `out (_io_border)` в `roomtest.c` (база модуля 0x42AD) плюс
резидентные пустышки `pop_dbg_m*` из `pop_state.c`. Адреса брать ЗАНОВО из
`.sprinter-cc-roomtest/roomtest.map` после каждой пересборки. Скрипты
сессии: `perfrun.py <out> <сек> tag=addr ...` и `parseseq.py <файл> ПОСЛЕД`.
Цену отдельной РЕЗИДЕНТНОЙ функции можно снять вообще без пересборки:
`bpset <вход>,1,{temp0=totalcycles; g}` плюс `bpset <точка>,1,{printf "…
%d",totalcycles-temp0; g}`. Так разложен блит в §1.
---
## 5. Сводка: что сколько даёт в 11/15
| # | приём | эффект | тип оценки | риск |
|---|---|---:|---|---|
| P1 | чомпер: только anim-слой | −160 000 | модель | низкий |
| P3 | Кид перестанет будиться | −70 000 | модель | следствие P1 |
| P2 | логика двух Char | 40 000 … 70 000 | гипотеза | ? |
| P4 | накладные блита (4 правки) | −28 000 | модель | низкий |
| P5 | `loose_tick` без плит | ✅ −33 840 | ФАКТ | сделано |
| P6 | цикл `process_trobs` | 20 000 … 40 000 | гипотеза | ? |
| P10 | футпринт из физики | −23 000 | замер | средний |
| P11 | мелочи (4 штуки) | −26 000 | замер | низкий |
| P12 | снять оснастку | −6 000 | замер | нулевой |
| P7 | раскол `draw_tile` | 0 здесь (50 000 в 13/23) | оценка | средний |
| P8/P9 | HEAL-WIDTH / G8 | 0 здесь (сцены с плитами) | оценка | низкий/средний |
Верхняя часть списка (P1 + P3 + P4 + P5) — **около 286 000 из 801 768, то
есть 36 % работы кадра**, и вся она низкого риска. Этого хватит, чтобы
сцена ушла с 1,86 растрового кадра до ~1,2 — но НЕ хватит, чтобы период
кадра упал с 4 растров до 3: для этого работа должна уложиться в 430 000,
то есть нужны ещё ~90 000 сверху (P2 или P6).
---
## 5б. Цианная фаза разложена [ЗАМЕР 2026-08-19]
Фаза 188 004 тактов, и она НЕ менялась ни от P1, ни от P5, ни от P2b.
| участок | такты |
|---|---:|
| `check_mirror` | 3 198 |
| `loose_mob_draw` + `guard_over_kid` + `skip_mask` | **34 374** |
| **`pop_char_draw(KID)`** | **204** |
| **соперник: `char_draw` + `char_fore`** | **145 896** |
| `fore_needed` + `hp_draw` | 2 598 |
| `char_fore(KID)` + борта | 1 758 |
**Кид уже пропускается** — 204 такта, то есть надежда P3 всё-таки сбылась
после P1: метка от чомпера до него больше не дотягивается. А страж
перерисовывается каждый кадр, и это ЧЕСТНО: пламя правого факела (0,7)
рисуется в ячейке (0,8), где он стоит, и реально накрывает ему голову
(пламя занимает y 5..22, страж 12..62).
Отрисовка стража (148 302) по частям:
| участок | такты | доля |
|---|---:|---:|
| **`pop_char_fore`** (два трамплина в банк 2 + обход тайлов) | **62 778** | 42 % |
| клинок: `pop_sword_draw` + `cd_overlay_add` + `cd_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` (8 658), и убрать его непросто: функции нужны
`get_tile` и таблицы деления из банка 3, а перенос в резидент вернёт тот же
трамплин внутрь. Всё остальное — работа, которую персонаж действительно
делает: рисует себя, клинок и передний слой поверх себя.
## 6. Иерархия референсов (уточнена 2026-08-19)
Сравнение трёх реализаций луча видимости показало, что источники не
равноценны, и это важно для ЛЮБОЙ будущей оптимизации:
| источник | что берём | чего НЕ берём |
|---|---|---|
| **Apple II** (`Prince-of-Persia-Apple-II`) | как это делается на 8 битах: таблицы вместо делений, борьба за такты | ничего — но код на 6502, читать сложнее |
| **SDLPoP** | эталон ПОВЕДЕНИЯ (декомпиляция DOS-версии) | реализацию: она нарочно «расслаблена» под 32 бита |
| **mininim** | разбор краевых случаев, второе мнение о замысле | алгоритмы — переписан с нуля, механика местами своя |
Доказательство на конкретном месте: `get_tile_div_mod` в SDLPoP содержит
комментарий
```c
// DOS PoP does this:
// obj_xl = tile_mod_tbl[xpos];
// return tile_div_tbl[xpos];
```
а вместо этого делает `x % TILE_SIZEX` и `x / TILE_SIZEX`. Таблицы в файле
лежат, но нужны только для эмуляции чтения DOS-версии ЗА ГРАНИЦЕЙ массива.
Apple II (`CTRLSUBS.S`, `GETBLOCKX`) читает ровно `BlockTable[x]`.
**Правило:** сверять поведение по SDLPoP, а реализацию под 8 бит — по
Apple II и по комментариям вида «DOS PoP does this» в самом SDLPoP.
## 7. Повторяющийся источник цены: банковый трамплин в цикле
Уже трижды крупнейшей статьёй оказывался не алгоритм, а вызов `__banked`-
функции ИЗ ЦИКЛА, идущего в другом банке:
| место | цена | лечение |
|---|---:|---|
| луч видимости: `pop_tile_at` по колонке (P2b) | 36 786 → 13 002 | один вызов на весь отрезок |
| `pop_clip_char_top` — банк 4 → банк 3 ради одной проверки | 8 892 | не сделано (P11) |
| `pop_trob_modif(room)` на каждый trob в цикле | не мерено | не сделано (P6a) |
**Что проверять в первую очередь при новом «дорогом» месте:** не сколько
там арифметики, а сколько раз за кадр пересекается граница банка.