# Реестр оптимизаций: всё отложенное, в одном списке Собрано 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.
Исходная (неполная) постановка Разбор в [`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.
### 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), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже перерисовывает. ### P17. 16 бит там, где хватает 8 ✅ 2026-08-19 (замечание пользователя) **1. Границы экрана — беззнаковыми сравнениями.** Проверка «спрайт целиком на экране» стояла как четыре ЗНАКОВЫХ 16-битных сравнения, а знаковое у SDCC z80 разворачивается в `sbc` плюс `jp PO / xor 0x80 / jp P`. Беззнаковая форма делает то же двумя: отрицательная координата становится очень большой и проваливает условие так же, как `>= 0`. **−378.** **2. Габариты спрайтов в байтах.** `w`/`h`, `ow`/`oh`, `fpw`/`fph`, `cw`/`ch` в `pop_cdraw_t`, параметры `cd_overlay_add`/`cd_clip_add`, локали в `pop_char_draw`/`cd_splash` и чтение габарита из шапки ленты были `uint16_t`, хотя спрайты атласов не крупнее 64×64 (memory `pop_sprite_size_limits`). **−276 в статике, −1 134 в циане динамики**, плюс 24 байта `_DATA`. ### P6a. Кэш указателя модификаторов ✅ 2026-08-19 — −840 (ждали −20 000) `pop_trob_modif` объявлен `__banked`, а звался на КАЖДЫЙ trob внутри цикла `pop_process_trobs`, хотя комната у них в подавляющем большинстве кадров одна. Указатель теперь кэшируется между итерациями. Цикл trobs 78 726 → **74 964**, работа кадра 438 324 → **437 484**. **Оценка в реестре была завышена в двадцать раз**, и стоит понять почему: я перенёс её по аналогии с лучом видимости (P2b), где трамплин звался ДЕВЯТЬ раз за кадр. Здесь trob'ов в комнате всего несколько, и кэш экономит два-три вызова. **Урок: «тот же паттерн» не означает «тот же порядок величины» — считать надо число вызовов, а не узнавать шаблон.** ### P6b. Кэш префетча кодов тайлов — НЕ ДЕЛАЛОСЬ Префетч (`pop_level_access_begin/end` плюс чтение кода на каждый trob) стоит **11 058** за кадр. Кэшировать мешает инвалидация: код тайла меняет `pop_level_set_tile` (кнопка → пол, loose → empty), вход в комнату и добавление trob'а — пропустить хоть один источник значит получить застывшую анимацию. С учётом того, что P6a дал 840 вместо 20 000, ожидаемый выигрыш тут тоже стоит считать скромным, а риск он несёт несоразмерный. ### P18. Метка «фон трогали» огрублена по X — ОТЛОЖЕНО (решение пользователя) **Найдено 2026-08-19 пользователем:** Кид перерисовывается, хотя с пламенем не пересекается; на пиксель левее — перестаёт. Разбор по памяти машины. Кид `x = 156`, спрайт занимает **x 213..224**, экранные y 43..83. Метка колонки 7 — y 33..50 (пламя правого факела). Колонка считается как `x >> 5`, то есть по 32 пикселя, и спрайт достаёт до 224 — ровно первый пиксель колонки 7. По вертикали пересечение с меткой настоящее (43..50), поэтому слот считается задетым. А по горизонтали пересечения НЕТ: пламя лежит в колонке 7 на x 232..247, между ним и Кидом восемь пикселей зазора. На пиксель левее спрайт кончается на 223, `223 >> 5 = 6`, колонка 7 не задета — и перерисовка пропадает. То есть P15 исправил огрубление по Y и оставил его по X. **Почему отложено (аргументы пользователя):** - x лежит в 0..319 и в байт не влезает — нужен `uint16_t` на границу, то есть 4 байта на колонку (80 байт на две страницы), и **16-битные сравнения в горячем пути**. А они у SDCC z80 дороги ровно настолько, что могут съесть весь выигрыш (см. отрицательные результаты выше); - огрубить x вдвое (`x >> 1`, диапазон 0..159 влезает в байт) — это лишний сдвиг и при записи, и при проверке, плюс точность падает до 2 пикселей. **Непроверенная идея на будущее:** хранить границы НЕ в экранных x, а как смещение ВНУТРИ колонки (0..31, пять бит). Тогда байта хватает и сравнение 8-битное, но запись усложняется: прямоугольник, пересекающий несколько колонок, даёт частичные диапазоны у крайних и полные у средних. **Когда браться:** если после других позиций бюджет всё ещё не сойдётся. Выигрыш будет именно в пограничных положениях, а их в игре много — персонаж почти всегда стоит рядом с чем-то анимированным. ### P4. Накладные блита — ОТКАЧЕНО **Правка сделана и отменена по решению пользователя.** Критерий: если выигрыш получен ценой сильно усложнённого кода — откатывать. Что было: `atlas_image_w0` в libbgi читал каталог из уже подключённой в W0 страницы. **−408 на кадре** при ожидании −5 400. Почему откачено: цена — вторая публичная функция в API libbgi с НЕЯВНЫМ контрактом («страница обязана быть подключена до вызова»), которую легко вызвать неправильно и молча получить мусор, плюс дублирование чтения каталога. 408 тактов — 0,09 % кадра, меньше разброса между прогонами. **Что осталось знанием:** сам `gfx_w0_map` стоит всего **324** такта, а 672 у `atlas_image` — это почти целиком вызов функции и арифметика `idx * 8`. Значит непробованная часть P4 («один map на группу блитов») имеет потолок ~2 600 за кадр, а не 10 000, как считалось. Ожидание было −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, как считалось.
Исходная постановка (модель −28 000) | правка | на блит | источник | |---|---:|---| | `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 фиксированных.
### 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~~ | | | | правка **ОТКАЧЕНА** | | после P17, лёгкая | 438 324 | 217 590 | 181 098 | 39 192 | 4 | | **после P6a, лёгкая** | **437 484** | 217 704 | 180 252 | 39 306 | 4 | | **после P17, тяжёлая** | **603 684** | 241 956 | 181 464 | 180 270 | 4 | Итог восьми позиций: **801 768 → 464 796 в лёгкой позиции (−42 %)** и **758 358 → 617 487 в тяжёлой (−19 %)**. Отдельно важно: в тяжёлой позиции исчезли пятирастровые кадры (было 27 %), период стал ровно 4 — рывки ушли. ### Достижима ли цель — арифметика на 2026-08-19 Цель: работа ≤ 430 000, тогда период станет 3 растра (хвост кадра — три `gfx_wait_vsync`). - в ЛЁГКОЙ позиции снять надо **7 484**; - в ТЯЖЁЛОЙ — **173 684**. **Лёгких путей больше не осталось.** За 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 <сек> 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) | **Что проверять в первую очередь при новом «дорогом» месте:** не сколько там арифметики, а сколько раз за кадр пересекается граница банка.