6d7c1c8b6b
Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где не анимируется ни одна плита и не летит ни один кусок. Раскладка зондами m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск куска над головой Кида 5 868 (там ещё и банковый трамплин). Два гейта: loose_any (статик pop_map.c) — «идёт ли анимация плит». Ставят пять мест записи ненулевой фазы: make_loose_fall, ветка потолка в check_press, do_knock для обоих рядов и восстановление фазы из room_modif при входе в комнату. Снимает его сам цикл, по факту прохода, в котором не осталось ни одной живой фазы. pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска» (active или дочистка clean). Ставит mob_alloc, снимает обход по факту пустой таблицы. В резиденте, а не в pop_room.c, потому что читает его pop_map из банка 3, а писучие статики банкового модуля наружу не видны. Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя). Асимметрия намеренная — ложная единица стоит одного холостого прохода, ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту пустого прохода. Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760. На кадре работа 801 768 -> 767 928. Ожидание по реестру было -28 000. Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода (фаза восстановлена входом в комнату) — единственное, которое не прогонял ни один тест, и дающее самый тихий отказ. Мутационная проверка: со снятым взводом тест падает (фаза 3 вместо 4). Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2 (дефолт), roomtest стартует в 11/15 с Кидом в (0,2). kid_init ставит x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё физика относила Кида в тайл чомпера, и он погибал на старте (найдено пользователем). Сдвиг внутрь на 2: колонку определяет весовая точка кадра, поэтому число снято замером, а не выведено геометрией. План работ между сессиями — docs/perf_registry.md §4: очередь позиций со статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
299 lines
20 KiB
Markdown
299 lines
20 KiB
Markdown
# Реестр оптимизаций: всё отложенное, в одном списке
|
||
|
||
Собрано 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. Логика двух Char — 152 190 тактов БЕЗ единого блита [гипотеза: −40 000 … −70 000]
|
||
|
||
`m1→C`: `kid_tick`, `pop_phys_tick`, `pop_guard_tick`, `pop_guard_phys_tick`,
|
||
боёвка, `pop_check_can_guard_see_kid`. Графики там нет вообще, а стоит это
|
||
19 % работы кадра — столько же, сколько вся отрисовка обоих персонажей.
|
||
|
||
**Сначала замер** (зонды внутрь синей фазы, нужна пересборка), потом решение.
|
||
Кандидаты по прошлым находкам: `sdcc_z80_stack_locals_hot_loop` (локали в
|
||
IX-фрейме), деления в луче видимости, банковые трамплины на каждый шаг.
|
||
|
||
Новое, найдено 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` — 59 100 без блитов пламени [гипотеза: −20 000 … −40 000]
|
||
|
||
Префетч кодов тайлов делает `pop_level_access_begin/end` (маппинг окна 0)
|
||
каждый кадр; дальше switch по всем trob'ам комнаты. Надо померить, сколько
|
||
там trob'ов и куда уходит время.
|
||
|
||
Новое, найдено 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 | замер логики двух Char | ⬜ следующее | нужна пересборка с зондами |
|
||
| P6 | замер цикла `process_trobs` | ⬜ вместе с P2 | та же пересборка |
|
||
| P1 | чомпер под факелом: только слой anim | ⬜ | ждём −160 000 |
|
||
| P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 |
|
||
| P11 | мелочи с известной ценой | ⬜ | ждём −26 000 |
|
||
| P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 |
|
||
| P7 | раскол `draw_tile` (G5) | ⬜ | для 13/23, не для 11/15 |
|
||
| P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами |
|
||
| P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить |
|
||
|
||
**Текущее состояние бюджета 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 <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).
|