Документы по фазам: результаты дня и оставшийся план

Зелёная и циан — раскладка ПОСЛЕ правок, журнал правок с коммитами,
отрицательные результаты с объяснением причины (окно клипа в
pop_floor_bake — три попытки), и что осталось: раскол draw_tile на девять
узких частей по образцу seg008:01C7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:23:38 +03:00
parent af189a1d92
commit 14e183108d
3 changed files with 294 additions and 293 deletions
+103 -128
View File
@@ -13,169 +13,137 @@
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** **Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
| состояние | циан | | состояние | было (2026-08-17, `c312e4a`) | стало (`af189a1`) |
|---|---:| |---|---:|---:|
| покой, Кид пропущен (`pop_char_skip_mask`) | 20 550 28 700 | | покой, Кид пропущен | 20 550 | 20 550 |
| Кид перерисовывается, кусков нет | 107 000 193 000 | | Кид перерисовывается, кусков нет | 193 000 | 192 000 |
| **пик каскада (6 кусков в воздухе + Кид)** | **632 000 (1,6× бюджета)** | | **пик каскада (6 кусков + Кид)** | **631 800** | **270 510 ✔** |
**ЦЕЛЬ ФАЗЫ ВЫПОЛНЕНА** (270 510 при бюджете 400 000, запас 32 %).
--- ---
## 1. Раскладка (замер 2026-08-17, `c312e4a`) ## 1. Что решило дело
### 1.1 Подфазы (три готовых `PROF(6)`: 0x4BCE / 0x4C26 / 0x4C93) ### C1. Пометки «фон трогали» в `mob_render` подавлены — 55 000
| участок | покой | пик (кадр 20) | максимум | `pop_loose_mob_tick` помечает **весь коридор** куска одним вызовом, а три
|---|---:|---:|---:| блита внутри `mob_render` метили подмножества того же прямоугольника по
| `pop_check_mirror` + `pop_loose_mob_draw` (куски ПОД Кидом) + соперник | 23 250 | 34 950 | **154 512** | **4 502 такта** каждый. Механизм — `pop_cd_mute()`/`pop_cd_unmute()`
| `pop_char_draw(KID)` + `pop_loose_mob_draw_over` + `pop_fore_needed` + HP | 3 726 | **445 284** | 445 284 | `pop_tile.c`; отдельное значение того же флага `pop_cd_batch`, чтобы у
| `pop_char_fore(KID)` + `pop_cd_clear` + чистка бортов | 1 722 | **150 978** | 150 978 | `pop_cd_touch` на общем пути осталась ОДНА проверка).
### 1.2 Блиты кусков Добавлена пометка в `mob_spawn_copy`: кусок, рождённый ВНУТРИ тика
(`loose_fall` сбил плиту), получает слот с начала таблицы, то есть уже
пройденный циклом, — своей пометки в этом кадре он бы не получил, а нарисован
был бы. Без этого пропущенная пометка = стёртый и не перерисованный
персонаж.
Блитов в циане — **19 на кадр**: шесть кусков × три части спрайта ### C4. Кусок клипуется САМ, вместо чистки бортов после — −138 000
(`env 74` = 32×3, `env 70` = 32×13, `env 72` = 26×16, порт `draw_mob`
seg007:13E5 / индексы `loose_fram_*[10]`).
| | такты | Самая крупная и самая неожиданная статья. В `mob_render` стоял
|---|---:| `pop_clip_sprite`, то есть кусок рисовался в борт целиком и взводил
| Σ 19 блитов | **313 758** | `border_dirty`; `pop_room_clip_borders` потом стирал ДВЕ полосы во всю ширину
| на вызов | **16 273** | экрана (320×28 и 320×28) — **150 978 тактов в КАЖДОМ кадре**, пока хоть один
кусок торчит выше поля. А гряда 13-го уровня рождается ровно у потолка
(`y = 2`), то есть почти весь каскад. Стало 1 722.
То есть **половина фазы — это девятнадцать вызовов `pop_blit_b`.** Теперь окно клипа (`pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H)`)
ставится ТОЛЬКО когда кусок реально задевает борт: внутри поля блиты идут
быстрым путём.
### 1.3 Внутри одного `pop_blit_b` (157 замеров быстрого пути, зонды b1..b5) ### Композит куска: три блита → один — −163 000
Части `env 70 / 74 / 72` складываются в ОДИН getimage-блоб при загрузке
тайлсета (`mob_spr_build` в `pop_room.c`). Мотив прямо из
[[blit_cost_model]]: у блита ~8 800 такта постоянных накладных против ~5 000
на пиксели, а шесть кусков в воздухе давали 18 вызовов = **258 708 такта**,
больше половины фазы.
Тонкости, которые пришлось соблюсти:
- части **перекрываются** (74 и 70 обе идут от `mob_x`), поэтому композит
собирается попиксельно с пропуском `0xFF` — ровно как три прозрачных блита
друг поверх друга;
- габариты частей **читаются**, а не берутся константами: у тайлсетов правая
часть разная (26 px в подземелье, 25 во дворце);
- блоб лежит в обычной памяти (W2), поэтому блит идёт мимо `atlas_image` и
`gfx_w0_map/unmap` — ещё ~1 350 такта на вызов. Новый резидентный лист
`pop_mem_b` (`pop_tile.c`);
- страйд блоба = его ширина; сначала считается точный габарит, потом
копирование. Промежуточная версия объявляла блоб шириной 63 при
фактических 58 и переносила пять прозрачных колонок на каждом кадре;
- собирается на КАЖДУЮ смену тайлсета; резервный путь на три блита остался
(`mob_spr_ok`).
### Общие правки, попавшие и в эту фазу
- `blit_b_clip`: байтовый габарит + file-scope вместо локалей (кадр 22 → 12 Б,
обращений `(ix)` 211 → 51). Подробности — в
[`perf_green_phase.md`](perf_green_phase.md) §G3.
- `pop_blit_b`: аргументы в file-scope (76 → 11 обращений `(ix)`).
---
## 2. Раскладка на 2026-08-17 (до правок) — для истории
Подфазы (три готовых `PROF(6)`: 0x4BCE / 0x4C26 / 0x4C93), пик:
| участок | покой | пик |
|---|---:|---:|
| `pop_check_mirror` + `pop_loose_mob_draw` + соперник | 23 250 | 154 512 |
| `pop_char_draw(KID)` + `pop_loose_mob_draw_over` + `pop_fore_needed` + HP | 3 726 | 445 284 |
| `pop_char_fore(KID)` + `pop_cd_clear` + **чистка бортов** | 1 722 | 150 978 |
Разбор одного `pop_blit_b` (157 замеров быстрого пути, зонды b1..b5):
| участок | такты | доля | | участок | такты | доля |
|---|---:|---:| |---|---:|---:|
| `atlas_image` + `gfx_w0_map` + чтение габарита | 1 086 | 7 % | | `atlas_image` + `gfx_w0_map` + чтение габарита | 1 086 | 7 % |
| ядро блита (libbgi, `gfx_blit_noclip`) | 10 422 | 64 % | | ядро блита (libbgi, `gfx_blit_noclip`) | 10 422 | 64 % |
| **`pop_cd_touch` — пометка «фон тронут»** | **4 502** | **28 %** | | `pop_cd_touch` — пометка «фон тронут» | 4 502 | 28 % |
| `gfx_w0_unmap` + возврат | 264 | 2 % | | `gfx_w0_unmap` + возврат | 264 | 2 % |
| ИТОГО | 16 273 | | | ИТОГО | 16 273 | |
Для сравнения, клипованный путь (227 замеров, полоса у потолка): ядро Клипованный путь тогда же: ядро `blit_b_clip` 14 088, итого 16 409.
`blit_b_clip` **14 088**, итого на вызов **16 409**. После правок: клипованный блит 11 848, быстрый ~13 900 (у него больше
пикселей).
По модели `blit_cost_model` «железный» минимум передачи для этих трёх
спрайтов — 5 000 … 6 200 тактов. Остальные ~10 000 на вызов — накладные.
### 1.4 `pop_char_fore(KID)` вырастает в каскаде на два порядка
В покое 1 722, в кадрах каскада **150 978**. Кид стоит у правого края и НЕ
двигается, плиты падают в колонках 2..7 — то есть либо пропуск персонажа
(`pop_char_skip_mask`) гасится пометками `pop_cd_touch` от кусков, либо окно
fore-клипа расширяется и в него попадают лишние тайлы. **Не разобрано**
см. C4.
### 1.5 Кодоген
| функция | стековый кадр | обращений `(ix)` | ожидаемо тактов |
|---|---:|---:|---:|
| `blit_b_clip` (резидент) | **22 Б** | **211** | ~9 700 |
| `pop_blit_b` (резидент) | 6 Б | 78 | ~3 600 |
| `pop_cd_touch` (резидент) | — | 30 | ~1 380 |
| `mob_draw_pass` (bank 7) | 18 Б | 20 | ~900 |
| `mob_render` (bank 7) | — | 1 | — |
Одно `-N(ix)` ≈ 19 номинальных тактов ≈ 46 замеренных
(memory `sprinter_wait_states_2x`).
--- ---
## 2. Способы ускорения ## 3. Что осталось в запасе (если понадобится ещё)
По убыванию отдачи. Фаза в бюджете, поэтому это задел, а не план.
### C1. Убрать избыточный `pop_cd_touch` в `mob_render`
**Отдача: 81 000 (13 % фазы). Самая дешёвая правка из всех.**
`pop_loose_mob_tick` уже помечает **весь коридор куска** одним вызовом
`pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64)`с запасом на
путь, пройденный за кадр. А потом три блита внутри `mob_render` помечают
подмножества этого же прямоугольника, по 4 502 такта каждый: 3 × 4 502 × 6
кусков = **81 000 тактов впустую**.
Механизм уже есть: `pop_cd_batch_begin()` / `pop_cd_batch_end()`
`pop_tile.c`, применяется в `draw_tile`) — в пакете `pop_cd_touch` только
копит общий прямоугольник четырьмя сравнениями. Обернуть `mob_render` в
скобку.
Осторожно: пометка обязана покрывать **весь коридор heal'а**, а не габарит
спрайта — иначе персонаж, попавший в расширенную часть коридора, стирается,
но перерисовать себя не просит и исчезает с экрана (ровно этот баг ловили
2026-08-13: в комнате 23 после падения гряды пропадал Кид). Поскольку
коридорную пометку из тика мы НЕ трогаем, инвариант сохраняется.
### C2. `pop_cd_touch` — байтовые аргументы
**Отдача: часть от 4 502 на вызов; после C1 в циане останется 6 вызовов
(по одному на кусок), но в зелёной фазе и в fore-проходе их больше.**
Четыре `int`-аргумента на вызов, 30 `(ix)` внутри. `w`/`h` заведомо ≤ 255
(габарит спрайта ≤ 56×63), `x`/`y` — экранные, 16-битные. Это позиция (а)
задачи OPT-BLIT.
### C3. `blit_b_clip` — 22 Б кадра, 211 `(ix)`
**Отдача: 14 088 → ~10 500 на клипованный блит.** В циане клипованный путь
берут спрайты у краёв поля и весь fore-проход; в зелёной фазе — все блиты
полосы у потолка. Подробнее — [`perf_green_phase.md`](perf_green_phase.md) §G3.
`sx/sy/dw/dh``uint8_t`, `dx/dy` оставить 16-битными. Прецедент: тот же
приём в `pop_blit_b` (коммит `c312e4a`) дал −19 % на клипованном пути.
### C4. Разобрать `pop_char_fore(KID)`: 1 722 → 150 978
**Отдача неизвестна, но это 24 % фазы — мерить обязательно.**
Гипотезы (проверять зондами, не рассуждением — memory
`defer_unexplained_quirks`):
1. пометки `pop_cd_touch` от кусков гасят `pop_char_skip_mask` для Кида, хотя
куски падают в других колонках — проверить `pop_cd_dmask` в кадре;
2. `mob_mark_neighbour` ставит `pop_set_redraw_fore` на 1–2 тайла на каждый
кусок каждый кадр (порт `draw_mob`, seg007:1147), и `pop_fore_needed`
перерисовывает передние части 6–12 тайлов;
3. окно fore-клипа Кида расширено под клинок и брызги, и в него попадает
больше тайлов, чем нужно — известный пункт 1 в `perf_backlog.md`
(«футпринт персонажа — брать из физики»): оригинал расширяет футпринт
только на одну колонку при вынутом мече и объединяет с футпринтом прошлого
кадра, а мы расширяем окном клипа.
Зондов на подфазы циана больше нет — понадобятся временные `pop_dbg_*`
вокруг `pop_fore_needed` и `pop_char_draw`, то есть пересборка (после неё
ВСЕ адреса зондов меняются, брать заново из `roomtest.map`).
### C5. Один `gfx_w0_map`/`unmap` на группу блитов ### C5. Один `gfx_w0_map`/`unmap` на группу блитов
**Отдача: 1 086 + 264 = 1 350 на вызов; три части одного куска лежат в одной 1 086 + 264 на вызов. Для композита куска уже не нужно (он в обычной
странице атласа → −2 700 на кусок, −16 000 на кадр.** памяти), но остаётся для тайлов фона: куски одного тайла часто лежат в одной
странице атласа. Мешает то, что `pop_blit_b` — общий лист для всех
Позиция 3 старого `perf_backlog.md`. Нужна форма «открыть страницу, N вызывающих; нужна форма «открыть страницу, N блитов, закрыть».
блитов, закрыть»; мешает то, что `pop_blit_b` — общий лист для всех
вызывающих. Для `mob_render` (три блита из одного атласа `pop_env`) частный
случай тривиален.
### C6. Размеры ленты — из каталога атласа ### C6. Размеры ленты — из каталога атласа
**Отдача: часть от 1 086 на вызов.** Позиция 2 старого `perf_backlog.md`:
`fw`/`fh` уже лежат в записи каталога (8 байт: `offset u16, fw u8, fh u8, `fw`/`fh` уже лежат в записи каталога (8 байт: `offset u16, fw u8, fh u8,
nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = ny = 1`, то есть они nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = ny = 1`, то есть они
в точности равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает. равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает.
### C7. objtable вместо отдельного fore-прохода на персонажа ### C7. objtable вместо отдельного fore-прохода на персонажа
Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг, браться только Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг. Оригинал
если после C1–C5 бюджет всё ещё не выполняется. Оригинал кладёт персонажей и кладёт персонажей и куски в `objtable` и рисует их при обходе тайлов
куски в `objtable` и рисует их при обходе тайлов (`draw_objtable_items_at_tile`), порядок окклюзии получается сам; у нас
(`draw_objtable_items_at_tile`), а порядок окклюзии получается сам; у нас
отдельный fore-проход НА КАЖДОГО персонажа. отдельный fore-проход НА КАЖДОГО персонажа.
### Снять временную оснастку
Шесть вызовов `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит
(`call` + `ret` × 6), плюс `pop_dbg_kind`/`m16` в `pop_redraw_needed`.
Снимать ПОСЛЕ того, как оптимизация закончена: без них не мерить.
--- ---
## 3. Что НЕ делать ## 4. Что НЕ делать
- **Не ставить W3-скобку из кода с `--w3`** — белый экран - **Не ставить W3-скобку из кода с `--w3`** — белый экран
(memory `gfx_blit_noclip_fast`). (memory `gfx_blit_noclip_fast`).
@@ -185,11 +153,18 @@ nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = n
(`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены, (`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены,
буквальные 40 экранных пикселей срезают правый задний угол плиты буквальные 40 экранных пикселей срезают правый задний угол плиты
(прогон 2026-08-13). (прогон 2026-08-13).
- **Не сужать коридор heal «по палаццовому следу»** — габариты частей у
тайлсетов разные; брать высоту СОБРАННОГО композита (так и сделано).
--- ---
## 4. Журнал правок ## 5. Журнал правок
| дата | что сделано | циан: покой / Кид / пик | коммит | | дата | что сделано | циан: покой / Кид / пик | коммит |
|---|---|---|---| |---|---|---|---|
| 2026-08-17 | базовый замер (до правок этой серии) | 20 550 / 193 000 / **632 000** | `c312e4a` | | 2026-08-17 | базовый замер | 20 550 / 193 000 / **631 800** | `c312e4a` |
| 2026-08-17 | C1 подавление пометок в `mob_render` + пометка в `mob_spawn_copy` | — / — / **577 050** | `a3c473d` |
| 2026-08-17 | C4 кусок клипуется сам вместо чистки бортов; `blit_b_clip` байты+file-scope | — / — / **438 546** | `a3c473d` |
| 2026-08-17 | `pop_blit_b` аргументы в file-scope | — / — / **435 180** | `b2da0b8` |
| 2026-08-17 | **композит куска: один блит вместо трёх** | — / — / **273 078** | `b2da0b8` |
| 2026-08-17 | точный габарит композита (было 63 при 58) | 20 550 / 192 000 / **270 510 ✔** | `af189a1` |
+177 -161
View File
@@ -3,7 +3,8 @@
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды — запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные [`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные
габариты. Парная фаза — [`perf_cyan_phase.md`](perf_cyan_phase.md). габариты. Парная фаза — [`perf_cyan_phase.md`](perf_cyan_phase.md) (её цель
достигнута).
Границы фазы в `roomtest.c`: от `PROF(4)` (строка 450) до `PROF(6)` Границы фазы в `roomtest.c`: от `PROF(4)` (строка 450) до `PROF(6)`
(строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`, (строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`,
@@ -11,209 +12,224 @@
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** **Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
| состояние | зелёная | | состояние | было (`c312e4a`) | стало (`af189a1`) |
|---|---:| |---|---:|---:|
| покой в комнате 23 | 35 760 | | покой в комнате 23 | 35 760 | 35 760 |
| дрожат 6 плит-потолков | 366 000 | | дрожат 6 плит-потолков | 366 000 | 335 400 |
| **пик каскада (провалы + посадки)** | **805 000 (2,0× бюджета)** | | **пик каскада** | **805 000** | **546 900** |
**ЦЕЛЬ ФАЗЫ НЕ ДОСТИГНУТА: 546 900 против 400 000 (1,37×).** Что осталось
сделать и почему это именно раскол `draw_tile` — §3.
--- ---
## 1. Раскладка (замер 2026-08-17, `c312e4a`, зонды m5/m6/m7 + kind/m16) ## 1. Раскладка ПОСЛЕ правок (замер `af189a1`)
### 1.1 Верхний уровень Пик — кадры 24-28 (посадки плит), больше не кадры провалов.
| участок | покой | дрожь | пик | | участок | покой | дрожь | пик |
|---|---:|---:|---:| |---|---:|---:|---:|
| `pop_loose_tick` | 25 602 | 35 646 | **196 152** | | `pop_loose_tick` | 36 234 | 36 234 | **156 762** |
| `pop_process_trobs` | 1 050 | 1 050 | 1 050 | | `pop_process_trobs` | 1 050 | 1 050 | 1 050 |
| **`pop_redraw_needed`** | 924 | **319 944** | **737 322** | | **`pop_redraw_needed`** | 924 | 288 800 | **380 568** |
| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 11 964 | | хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 10 776 |
**`pop_redraw_needed` — 91 % фазы на пике.** `pop_loose_tick` изнутри на пике: два цикла по тайлам 9 852,
**`pop_loose_mob_tick` 154 074** (heal шести летящих кусков), остальное мелочь.
### 1.2 `pop_loose_tick` изнутри (зонды m9..m12) ### Цена одной перерисовки: было → стало
| участок | покой | пик | | вид | было | стало | чем |
|---|---:|---:| |---|---:|---:|---|
| два цикла по тайлам (30 + 10 позиций) | 11 004 | 9 852 | | `RDA_CEIL` — дрожащая плита-потолок | 48 785 | **43 536** | G2 + G3 |
| **`pop_loose_mob_tick`** (heal летящих кусков) | 7 416 | **178 494** | | `RDA_CEIL_GONE` — запечь колодец | 251 335 | **138 318** | **G1** (клип полосы) + G2 + G3 |
| `check_loose_fall_on_kid` | 6 120 | 3 480 | | `RD_FLOOR` — щебень на месте посадки | 198 805 | **179 914** | G2 + G3 + снятая двойная пометка |
| хвост | 324 | 3 252 |
`pop_loose_mob_tick` при шести кусках в воздухе — **≈29 700 такта на кусок**. Штук за кадр: `RDA_CEIL` до 6, `RDA_CEIL_GONE` до 2, `RD_FLOOR` до 2.
Это heal прошлой позиции (коридор `MOB_W × 64`) плюс `pop_cd_touch` на
коридор плюс `mob_tick_one`.
### 1.3 Цена ОДНОЙ перерисовки по видам (66 + 12 + 12 замеров) ### Детальный профиль `RD_FLOOR` (179 914) — главная оставшаяся статья
| вид | штук замерено | средняя цена | сколько за кадр | Σ за кадр | Снят зондами по каждому блиту (`pop_dbg_b1/b5`) и по рамкам
|---|---:|---:|---:|---:| (`pop_bar_black`, `pop_cd_batch_begin/end`):
| `RDA_CEIL` — дрожащая плита-потолок | 66 | **48 658** | до 6 | 292 000 |
| `RDA_CEIL_GONE` — запечь колодец | 12 | **251 023** | до 2 | 619 000 |
| `RD_FLOOR` — щебень на месте посадки | 12 | **198 259** | до 2 | 397 000 |
Для масштаба: ОДНА запечка колодца = 58 % растрового кадра, две = 144 %.
`pop_dbg_rdmax` подтверждает состав пика: шесть пометок, все `RDA_CEIL`.
### 1.4 `RDA_CEIL` = 48 658 изнутри (зонды m13/m14/m15)
| участок | такты | | участок | такты |
|---|---:| |---|---:|
| `pop_heal_off(x, 0, 64, CEIL_BAND_H=8)` | 8 491 | | вход `pop_floor_bake` + `gfx_set_bank` | 2 382 |
| `draw_tile(-1, col)` | **39 029** | | `pop_bar_black` 60×39 (включая `pop_cd_touch` 4 502) | ~15 500 |
| **контекст `draw_tile` #1** (5 чтений тайлов, `63*row`, индексация таблицы) | **13 584** |
| 4 блита тайла #1 | 50 718 |
| **диспетчер между блитами #1** (все `if (code == …)`) | **11 388** |
| **контекст `draw_tile` #2** | **13 584** |
| 4 блита тайла #2 | ~54 000 |
| **диспетчер между блитами #2** | **11 388** |
| хвост | 6 474 |
Блитов внутри этого `draw_tile`**1,17 на тайл** (7 вызовов `pop_blit_b` Итого: **105 500 — сами блиты (реальные пиксели), 74 400 — накладные**, из
на 6 тайлов), то есть 15–16 тыс. тактов. которых 27 168 контекст двух `draw_tile` и 22 776 их диспетчер.
**Остальные ~23 700 такта — накладные `draw_tile`, ни одного пикселя.**
### 1.5 `RDA_CEIL_GONE` = 251 023 и `RD_FLOOR` = 198 259
- `pop_ceil_bake_empty(col)` — бар + **шесть** `draw_tile`
(ряд −1 и ряд 0, колонки col1..col+1): 6 × 39 000 ≈ 234 000.
- `pop_floor_bake(row,col)` — бар 60×39 от `yb+26` + **два** `draw_tile`
НИЖНЕГО ряда; там стена, то есть `wall_pattern` — ≈**99 000 на тайл**.
### 1.6 Почему `draw_tile` стоит 39 000 при одном блите
Кодоген. Подсчёт в `bank7_pop_room.asm` и совпадение с замером до процентов
(одно `-N(ix)` ≈ 19 номинальных тактов ≈ 46 замеренных):
| функция | стековый кадр | обращений `(ix)` | ожидаемо | замерено |
|---|---:|---:|---:|---:|
| `draw_tile` | **26 Б** | **513** | ~23 600 | **~23 700** |
| `pop_ceil_bake_empty` | — | 8 | ~370 | |
| `pop_floor_bake` | — | 12 | ~550 | |
| `mob_tick_one` | 16 Б | 99 | ~4 550 | из ~29 700/кусок |
| `mob_draw_pass` | 18 Б | 20 | ~900 | |
У `draw_tile` пятнадцать локалей, объявленных `int`
(`code`/`lcode`/`lmod`/`xh`/`x`/`dby`/`dmy`/`t`/`lt`/`rbl_code`/`rbl_mod`/
`wmod`/`base_id`/`base_yb`), — в регистры они не влезли, и функция целиком
уехала в стековый кадр. Байтовые по природе из них все, кроме `x`/`dby`/`dmy`
(экранные координаты, бывают отрицательными).
--- ---
## 2. Способы ускорения ## 2. Что сделано (с чем сравнивать)
По убыванию отдачи. Оценки — от замеров выше; каждая позиция сверена с ### G1. Окно клипа для точечной перерисовки — −90 000
`SDLPoP/src/seg008.c` (правило проекта: механику сверять с оригиналом ДО
кодинга).
### G1. Окно клипа для ЗАПЕЧКИ — самый крупный кусок `pop_t_win_set(x, ytop, w, h)` / `pop_t_win_clear()` в `pop_tile.c`: ставит
уже существующее окно `pop_t_fclip_*` на прямоугольник, который перерисовка
восстанавливает. Работает в обе стороны — и предфильтр `pop_blit_b`
отсеивает куски мимо окна ДО `atlas_image`/`gfx_w0_map`, и `blit_b_clip`
режет остальные по нему.
**Отдача: 250 000 … −400 000 на пиковый кадр.** Где сработало: `pop_ceil_bake_empty` — куски ряда 0 высотой 63 px рисовались
целиком, хотя восстановить надо девять строк полосы. **Блит 21 447 → 7 619**,
вся перерисовка 251 335 → 138 318.
`pop_ceil_bake_empty` и `pop_floor_bake` восстанавливают ИЗВЕСТНЫЙ Где НЕ сработало — см. §4, отрицательные результаты.
прямоугольник (тот, который сами же залили баром), а зовут полный
`draw_tile`, который честно рисует и то, что в этот прямоугольник не попадает.
У `pop_floor_bake` бар — 60×39 от `yb+26`, а тело дворцовой кладки рисуется
НИЖЕ `yb+64`, то есть **вся стена рисуется мимо цели**.
В `pop_blit_b` уже есть дешёвый предфильтр по окну `pop_t_fclip_*` — он режет Побочно: пока окно стоит, `pop_blit_b` не ставит пометку «фон трогали»
кусок ДО `atlas_image` и `gfx_w0_map`, то есть почти даром. Достаточно (признак fore-прохода), поэтому вызывающий обязан пометить прямоугольник сам.
выставлять это окно на время запечки. В `pop_ceil_shake_draw` добавлен явный `pop_cd_touch` на область heal'а; в
`pop_ceil_bake_empty` и `pop_floor_bake` метит `pop_bar_black`, а лишний
второй вызов на ту же область снят.
Грабли, которые надо обойти: `pop_t_fclip_on` в `pop_blit_b` заодно подавляет ### G2. Контекст тайла — file-scope, а не локали `draw_tile` — 55 000
`pop_cd_touch` (признак «это fore-проход поверх персонажа»). Для запечки это
как раз правильно — она помечает область сама, — но проверить обязательно;
если понадобится, ввести отдельный флаг «окно только для отсева».
Сверка с оригиналом: у него та же идея, только через `wipetable` Порт `load_curr_and_left_tile` (seg008:0339): у оригинала это
`draw_tile_wipe(height)` кладёт прямоугольник в таблицу, а `draw_table` `curr_tile`/`curr_modifier`/`draw_xh`/`draw_main_y`/`draw_bottom_y`
рисует уже с известным клипом (seg008:1368, 1373). переменные модуля, а не локали.
### G2. Расколоть `draw_tile` + отдельный лист для ряда −1 Причина в кодогене: в `draw_tile` **57 вызовов**, и каждое живое через вызов
значение SDCC спиливал в стековый кадр — 26 байт кадра и **513 обращений
`-N(ix)`** (при ~46 замеренных тактах на обращение это ~23 600, что и
намерено). Стало **33 обращения**, кадра нет, банк 7 −703 Б.
**Отдача: 39 029 → 18 00020 000 на тайл полосы, то есть −140 000 на кадр ### G3. `blit_b_clip` — байтовый габарит + file-scope
дрожания; и она умножается на G1 (шесть вызовов в запечке).**
Два шага: Два шага, и важен порядок наблюдений:
1. **Отдельный `draw_tile_aboveroom(col)`** — дословный порт seg008:01F2. 1. **Байтового габарита ОДНОГО НЕ ХВАТИЛО.** `sx/sy/dw/dh``uint8_t`
Оригинал для ряда −1 делает РОВНО шесть вещей: `draw_tile_floorright`, (корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а
`draw_tile_anim_topright`, `draw_tile_right`, `draw_tile_bottom(1)`, 22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi,
`draw_loose(1)`, `draw_tile_fore`. Ни базы, ни `draw_tile_anim`, ни всё равно больше, чем регистров у Z80.
`draw_tile_anim_right`, ни `wall_pattern`, ни вычисления `rbl_*` через 2. **Решило вынесение из локалей** (`bc_*`): 51 обращение, кадр 22 → 12 Б.
`row+1 > 2`. У нас всё это отсекается ветками `if (row >= 0)` уже ВНУТРИ
общего `draw_tile` — то есть кадр IX, пролог и вся арифметика оплачиваются
всегда.
2. **Расколоть общий `draw_tile` по образцу оригинала** (seg008:01C7 —
девять узких листьев). Каждый лист получает `xh`/`dby`/`dmy` параметрами
(в регистрах), локалей у него единицы, кадра IX нет.
Заодно проверить `redraw_needed_above` (seg008:02C1): оригинал делает Клипованный блит 14 088 → 11 848. Заодно `blit_b_oversize` больше не ходит
`draw_tile_wipe(3)` — стирает **32×3**, а мы `pop_heal_off(x,0,64,8)` = через `blit_b_clip` (там теперь байтовый габарит) — рисует напрямую
64×8. Площадь в 5 раз больше нужной (8 491 такта), хотя основная цена там — `gfx_blit_part`; это путь под полноэкранные подложки интро/финала.
накладные вызова.
### G3. `blit_b_clip` — 22 Б кадра, 211 `(ix)` ### G4. Мелочи
**Отдача: 14 088 → ~10 500 на блит. В зелёной фазе ВСЕ блиты клипованные - `pop_blit_b`: аргументы в file-scope (третий и дальше SDCC передаёт стеком,
(полоса у потолка выставляет `pop_t_clip_top`) — при 7–17 блитах это каждое чтение шло через `-N(ix)`) — 76 → 11 обращений.
25 000 … 60 000; в циане и fore-проходе больше.** - `pop_loose_mob_tick`: пометки всех кусков ОДНИМ пакетом
(`pop_cd_batch_begin/end`) — было по 4 502 такта на кусок.
`dx/dy/dw/dh/sx/sy` объявлены `int`. По построению `sx/sy/dw/dh ≤ 255` 176 772 → 168 600.
(габарит любого спрайта ≤ 56×63, см. `perf_l13_room23.md` §5) — их можно - Коридор heal куска — по фактической высоте СОБРАННОГО композита (было 24
сделать `uint8_t`, оставив 16-битными только экранные `dx/dy`. строки константой, стало 20). 168 600 → 156 762.
Прецедент: тот же приём в `pop_blit_b` (коммит `c312e4a`) дал 19 % на
клипованном пути и −16 % на быстром, `_CODE` 42 Б.
Проверка по правилу OPT-BLIT: после правки смотреть пролог в
`.sprinter-cc-roomtest/pop_tile.asm` — исчез ли `ld iy,#-22`, сколько
осталось `-N(ix)`.
### G4. `mob_tick_one` / `pop_loose_mob_tick` — 29 700 на кусок
**Отдача: 30 000 … −48 000 на кадр при шести кусках.**
16-байтовый кадр, 99 `(ix)`. Байтовыми могут быть габарит heal-коридора и
разность `prev_y`; `x`/`y` — 16-битные. Плюс `pop_cd_touch` на коридор — один
на кусок, 4 502 такта (см. `perf_cyan_phase.md` §1.3 про его цену).
### G5. `LOOSE-SHAKE-RUNS` — пометки по сменам кадра
**Отдача: ~20 % работы фазы, и только на дрожащих плитах. Оценка старая
(2026-08-13), не перемерена.**
Подробности и арифметика — в `roomtest/TASKS_OPEN.md#loose-shake-runs`.
Коротко: наивный вариант «помечать только на смене кадра» выигрыша НЕ даёт
(пять смен × две страницы = те же десять перерисовок); работает только версия
«пары и тройки» — две пометки с `pages = 2` на фазах 5 и 8. Тот же приём
применим к плитам-ПОТОЛКАМ (`pop_set_redraw_above`), где сейчас идёт пометка
каждый кадр отсчёта.
Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться
после G1G4.
### G6. `BG-ONCE` — heal вместо чёрного бара
Предложение пользователя, см. `roomtest/TASKS_OPEN.md#bg-once`. Сейчас
запечка = «залить чёрным + перерисовать фон», то есть двойная работа; heal по
координатам бара делает то же за один проход. Прямо дополняет G1.
--- ---
## 3. Что НЕ делать (проверено, отрицательный результат) ## 3. Что осталось: раскол `draw_tile` (позиция G5)
**Оставшийся разрыв: −147 000.** Он весь в двух местах.
### G5. Расколоть `draw_tile` на узкие части, как в оригинале
**Ожидание: 50 000 … 60 000.**
У оригинала `draw_tile` (seg008:01C7) — это девять независимых вызовов:
`draw_tile_floorright`, `draw_tile_anim_topright`, `draw_tile_right`,
`draw_tile_anim_right`, `draw_tile_bottom`, `draw_loose`, `draw_tile_base`,
`draw_tile_anim`, `draw_tile_fore`. Для ряда −1 он зовёт шесть из них
(`draw_tile_aboveroom`, seg008:01F2), для полосы у потолка — те же шесть плюс
`draw_tile_wipe(3)` (`redraw_needed_above`, seg008:02C1).
У нас всё это — ветки `if (row >= 0)` ВНУТРИ одной функции, то есть контекст
(13 584) и диспетчер (11 388) оплачиваются целиком всегда. Расколов, каждая
точечная перерисовка сможет звать только нужные части:
- `pop_floor_bake`: вместо второго полного `draw_tile(row, col+1)` — только
его правую грань и базу;
- `pop_ceil_shake_draw` / `pop_ceil_bake_empty`: дословный
`draw_tile_aboveroom`;
- `pop_loose_shake_draw`, `pop_spike_redraw`, `pop_gate_redraw` — то же.
Риск средний: у `draw_tile` собрано много инвариантов (BUG-LOOSE-3,
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1), проверять придётся прогонами всех
уровней. Поэтому делать отдельным заходом, а не хвостом другой правки.
### G6. Меньше блитов в `RD_FLOOR`
**Ожидание: неизвестно, надо мерить.** 105 500 из 179 914 — это 7,6 блита,
и они рисуют настоящие пиксели. Сократить можно только сократив то, что
восстанавливается: бар сейчас 60×39 от `yb+26`, а плита занимает по вертикали
меньше (её куски: левая грань `POP_LOOSE_FRAM_LEFT` 32×13 на `dmy = yb+62`,
низ `POP_LOOSE_FRAM_BOTTOM` 32×3 на `dby = yb+65`, правая грань в соседе
26×16 на `dby1`). То есть плита живёт в `yb+47 .. yb+65`, а бар начинается с
`yb+26`**21 лишняя строка сверху**.
Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла
(орнаментная лента `stripe_id` на `dmy27 = yb+35` попадает как раз в
«лишнюю» часть). Сузишь бар — надо убедиться, что ничего не осталось.
### G7. heal летящих кусков — 154 074 (28 % фазы)
Шесть кусков × ~25 700: сам heal 64×20 (по модели ~19 700) + пакетная
пометка + накладные `mob_tick_one` (16-байтовый кадр, 99 обращений `(ix)`).
**Сам heal у предела железа** — это 1 280 пикселей на кусок, оптимизировать
нечего, кроме площади. Площадь уже подрезана до габарита композита.
Остаётся `mob_tick_one` (~4 500 на кусок = 27 000 на кадр) — то же лечение
file-scope, что у `draw_tile`.
### G8. Снять временную оснастку
Шесть `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит; при 15
блитах зелёной это 6 000 на кадр. Плюс `pop_dbg_kind`/`m16` (2 вызова на
перерисовку) и `pop_dbg_m5..m15`. Снимать ПОСЛЕ окончания оптимизации: без
них не мерить.
---
## 4. Отрицательные результаты — НЕ повторять
### Окно клипа в `pop_floor_bake` — проверено ТРИ раза, каждый раз хуже
| попытка | было | стало |
|---|---:|---:|
| до G3 | 187 266 | 198 279 |
| после G3 | 182 124 | 188 460 |
| после `pop_blit_b` file-scope, с детальным зондом | 179 914 | 188 417 |
Третья попытка объяснила причину: клипованный путь стоит **+1 500 такта на
КАЖДОМ** из 7,6 блитов (+11 400), а режет он только редкие высокие куски — в
трассе такие нашлись (34 878 → 25 872 и 28 818 → 24 090, всего 13 700), но в
среднем по 12 перерисовкам их нет. Запись стоит в коде.
### Прочее (проверено раньше)
- **Не ускорять передачу пикселей** — она на пределе железа (3+3 такта на
байт через акселератор), memory `blit_cost_model`. В пиковом кадре
«железный» минимум всех блитов ≈150 000 из 1 437 150.
- **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из - **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из
которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры.
(проверено 2026-08-13). - **Не батчить смежные колонки в `pop_ceil_shake_draw`** — плиты стартуют со
- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — пробовалось и случайными задержками, в кадре дрожат разрозненные колонки, пробег почти
убрано: плиты стартуют со случайными задержками, в кадре дрожат разрозненные всегда длиной в одну.
колонки, пробег почти всегда длиной в одну. - **Не ускорять передачу пикселей** — предел железа (3+3 такта на байт,
- **Не искать проблему в W3-скобке** (`_bgi_begin`/`_bgi_end` — пять memory `blit_cost_model`). В пиковом кадре «железный» минимум всех блитов
инструкций) и не списывать разброс на прерывания (внутри размерной группы ≈150 000 из 916 458.
разброс три такта). - **`LOOSE-SHAKE-RUNS`** (пометки по сменам кадра): наивный вариант выигрыша
НЕ даёт — пять смен × две страницы = те же десять перерисовок. Работает
только версия «пары и тройки», ~20 % и только на дрожащих плитах; оценка
2026-08-13, не перемерена. Подробности —
`roomtest/TASKS_OPEN.md#loose-shake-runs`.
--- ---
## 4. Журнал правок ## 5. Журнал правок
| дата | что сделано | зелёная: покой / дрожь / пик | коммит | | дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|---|---|---|---| |---|---|---|---|
| 2026-08-17 | базовый замер (до правок этой серии) | 35 760 / 366 000 / **805 000** | `c312e4a` | | 2026-08-17 | базовый замер | 35 760 / 366 000 / **805 000** | `c312e4a` |
| 2026-08-17 | G2 контекст тайла в file-scope | — / — / **747 954** | `a3c473d` |
| 2026-08-17 | G1 окно клипа в `pop_ceil_bake_empty` | — / — / **663 250** | `a3c473d` |
| 2026-08-17 | G3 `blit_b_clip` байты + file-scope | — / 335 400 / **575 730** | `a3c473d` |
| 2026-08-17 | `pop_blit_b` file-scope; пакетная пометка кусков | — / — / **557 706** | `b2da0b8` |
| 2026-08-17 | коридор heal по высоте композита | 35 760 / 335 400 / **546 900** | `9a50ab2` |
+14 -4
View File
@@ -100,6 +100,8 @@ verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP-
## 4. Сводка по кадрам ## 4. Сводка по кадрам
**Базовый замер (`c312e4a`, ДО оптимизации):**
| фаза каскада | работа | синяя | зелёная | циан | период (растр.) | | фаза каскада | работа | синяя | зелёная | циан | период (растр.) |
|---|---:|---:|---:|---:|---:| |---|---:|---:|---:|---:|---:|
| покой в комнате 23 | 190 860 | 134 550 | 35 760 | 20 550 | **3** | | покой в комнате 23 | 190 860 | 134 550 | 35 760 | 20 550 | **3** |
@@ -107,14 +109,22 @@ verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP-
| провалы + полёт, ПИК | **1 437 150** | 142 700 | **663 250** | **631 200** | **56** | | провалы + полёт, ПИК | **1 437 150** | 142 700 | **663 250** | **631 200** | **56** |
| максимум по секции | | 142 830 | **805 000** | **631 800** | | | максимум по секции | | 142 830 | **805 000** | **631 800** | |
Цель — каждая секция ≤ 400 000. **Синяя в норме**; зелёная 2,0× бюджета, **После оптимизации (`af189a1`, 2026-08-17):**
циан 1,6×. Логический кадр вместо 3 растровых занимает 5–6: в каскаде игра
идёт вдвое медленнее нормы. | максимум по секции | работа | синяя | зелёная | циан |
|---|---:|---:|---:|---:|
| было | 1 437 150 | 142 830 | 805 000 | 631 800 |
| стало | **916 458** | 142 830 | **546 900** | **270 510** |
| | 36 % | — | 32 % | 57 % |
Цель — каждая секция ≤ 400 000. **Синяя и циан в бюджете**; зелёная 1,37×,
остаток разобран в [`perf_green_phase.md`](perf_green_phase.md) §3 (нужен
раскол `draw_tile` на узкие части, как в оригинале).
Где что расходуется и как это чинить — в фазовых документах: Где что расходуется и как это чинить — в фазовых документах:
[зелёная](perf_green_phase.md), [циан](perf_cyan_phase.md). [зелёная](perf_green_phase.md), [циан](perf_cyan_phase.md).
### Общий вывод по пиковому кадру (1 437 150) ### Общий вывод по пиковому кадру базового замера (1 437 150)
| | такты | доля | | | такты | доля |
|---|---:|---:| |---|---:|---:|