# Реестр оптимизаций: всё отложенное, в одном списке Собрано 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. Чомпер под факелом — возвращать только слой anim [модель: −160 000, 20 % работы] Разбор в [`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. Новое, найдено 2026-08-19. ### 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. ### P3. Персонажи будятся каждый кадр [модель: −70 000, следствие P1] heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то, что `pop_char_skip_mask` не может пропустить ни Кида, ни стража: фон трогают каждый кадр. - **Кида** спасает P1: широкая пометка вокруг чомпера исчезнет; - **стража спасти нельзя** — пламя правого факела (0,7) рисуется в ячейке (0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже перерисовывает. ### P4. Накладные блита — четыре независимые правки [модель: −28 000 в 11/15; больше в сценах с 20+ блитами] | правка | на блит | источник | |---|---:|---| | `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. Крупные рефакторинги — брать, только если понадобится ещё запас - **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. Замер обязателен даже когда «очевидно» — из шести сделанных позиций две дали не то, что ожидалось. | # | что | статус | факт | |---|---|---|---| | P5 | `loose_tick`: гейты холостого хода | ✅ **сделано** 2026-08-19 | −33 840 на кадре (ждали −28 000) | | P2 | замер синей фазы | ✅ замерено 2026-08-19 | гипотеза «трамплины» отвергнута | | P6 | замер `process_trobs` | ✅ замерено 2026-08-19 | 11 058 префетч + ~43 000 цикл | | P1 | чомпер под факелом: только слой anim | ⬜ **следующее** | ждём −160 000 | | P2b | гейт луча видимости стража | ⬜ | ждём −30 000, риск низкий | | P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 | | P11 | мелочи с известной ценой | ⬜ | ждём −26 000 | | P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 | | P6a/P6b | `trob_modif` из цикла, кэш префетча | ⬜ | ждём −20 000 | | P2a | разбор `pop_phys_tick` (105 246 на двоих) | ⬜ | цена известна, причина нет | | P7 | раскол `draw_tile` (G5) | ⬜ | для 13/23, не для 11/15 | | P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами | | P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить | ### Достижим ли период 3 растра — арифметика после замеров Работа сейчас **767 928**, цель **430 000**, снять надо **338 000**. Что известно и посчитано: | источник | ожидание | |---|---:| | P1 (чомпер: только слой anim) | −160 000 | | P3 (Кид перестанет будиться, следствие P1) | −70 000 | | P2b (гейт луча видимости) | −30 000 | | P4 (накладные блита) | −28 000 | | P11 (мелочи) | −26 000 | | P10 (футпринт из физики) | −23 000 | | P6a+P6b | −20 000 | | **сумма** | **−357 000** | То есть цель достижима, но **впритык и только если сделать ВСЁ перечисленное**, причём половина суммы держится на одной позиции (P1). Запаса нет, и любая позиция, давшая меньше ожидаемого, возвращает нас к разбору `pop_phys_tick` (P2a) — 105 246 тактов на физику двух НЕПОДВИЖНЫХ персонажей, где цена измерена, а причина ещё не найдена. **Текущее состояние бюджета 11/15:** | | работа | синяя | зелёная | циан | период | |---|---:|---:|---:|---:|---:| | до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра | | после P5 | **767 928** | 285 864 | 294 384 | 187 764 | 4 растра | Цель — уложить РАБОТУ в 430 000 (тогда период станет 3 растра вместо 4; хвост кадра — три `gfx_wait_vsync`). Осталось снять **338 000**. ### Как воспроизвести сцену (важно для следующей сессии) Сборка стартует прямо в ней: `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).