diff --git a/applications/PoP/docs/perf_cyan_phase.md b/applications/PoP/docs/perf_cyan_phase.md index cc301f3..04b3c25 100644 --- a/applications/PoP/docs/perf_cyan_phase.md +++ b/applications/PoP/docs/perf_cyan_phase.md @@ -13,169 +13,137 @@ **Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** -| состояние | циан | -|---|---:| -| покой, Кид пропущен (`pop_char_skip_mask`) | 20 550 … 28 700 | -| Кид перерисовывается, кусков нет | 107 000 … 193 000 | -| **пик каскада (6 кусков в воздухе + Кид)** | **632 000 (1,6× бюджета)** | +| состояние | было (2026-08-17, `c312e4a`) | стало (`af189a1`) | +|---|---:|---:| +| покой, Кид пропущен | 20 550 | 20 550 | +| Кид перерисовывается, кусков нет | 193 000 | 192 000 | +| **пик каскада (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_check_mirror` + `pop_loose_mob_draw` (куски ПОД Кидом) + соперник | 23 250 | 34 950 | **154 512** | -| `pop_char_draw(KID)` + `pop_loose_mob_draw_over` + `pop_fore_needed` + HP | 3 726 | **445 284** | 445 284 | -| `pop_char_fore(KID)` + `pop_cd_clear` + чистка бортов | 1 722 | **150 978** | 150 978 | +`pop_loose_mob_tick` помечает **весь коридор** куска одним вызовом, а три +блита внутри `mob_render` метили подмножества того же прямоугольника по +**4 502 такта** каждый. Механизм — `pop_cd_mute()`/`pop_cd_unmute()` +(в `pop_tile.c`; отдельное значение того же флага `pop_cd_batch`, чтобы у +`pop_cd_touch` на общем пути осталась ОДНА проверка). -### 1.2 Блиты кусков +Добавлена пометка в `mob_spawn_copy`: кусок, рождённый ВНУТРИ тика +(`loose_fall` сбил плиту), получает слот с начала таблицы, то есть уже +пройденный циклом, — своей пометки в этом кадре он бы не получил, а нарисован +был бы. Без этого пропущенная пометка = стёртый и не перерисованный +персонаж. -Блитов в циане — **19 на кадр**: шесть кусков × три части спрайта -(`env 74` = 32×3, `env 70` = 32×13, `env 72` = 26×16, порт `draw_mob` -seg007:13E5 / индексы `loose_fram_*[10]`). +### C4. Кусок клипуется САМ, вместо чистки бортов после — −138 000 -| | такты | -|---|---:| -| Σ 19 блитов | **313 758** | -| на вызов | **16 273** | +Самая крупная и самая неожиданная статья. В `mob_render` стоял +`pop_clip_sprite`, то есть кусок рисовался в борт целиком и взводил +`border_dirty`; `pop_room_clip_borders` потом стирал ДВЕ полосы во всю ширину +экрана (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 % | | ядро блита (libbgi, `gfx_blit_noclip`) | 10 422 | 64 % | -| **`pop_cd_touch` — пометка «фон тронут»** | **4 502** | **28 %** | +| `pop_cd_touch` — пометка «фон тронут» | 4 502 | 28 % | | `gfx_w0_unmap` + возврат | 264 | 2 % | | ИТОГО | 16 273 | | -Для сравнения, клипованный путь (227 замеров, полоса у потолка): ядро -`blit_b_clip` **14 088**, итого на вызов **16 409**. - -По модели `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`). +Клипованный путь тогда же: ядро `blit_b_clip` 14 088, итого 16 409. +После правок: клипованный блит 11 848, быстрый ~13 900 (у него больше +пикселей). --- -## 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` на группу блитов -**Отдача: 1 086 + 264 = 1 350 на вызов; три части одного куска лежат в одной -странице атласа → −2 700 на кусок, −16 000 на кадр.** - -Позиция 3 старого `perf_backlog.md`. Нужна форма «открыть страницу, N -блитов, закрыть»; мешает то, что `pop_blit_b` — общий лист для всех -вызывающих. Для `mob_render` (три блита из одного атласа `pop_env`) частный -случай тривиален. +1 086 + 264 на вызов. Для композита куска уже не нужно (он в обычной +памяти), но остаётся для тайлов фона: куски одного тайла часто лежат в одной +странице атласа. Мешает то, что `pop_blit_b` — общий лист для всех +вызывающих; нужна форма «открыть страницу, N блитов, закрыть». ### C6. Размеры ленты — из каталога атласа -**Отдача: часть от 1 086 на вызов.** Позиция 2 старого `perf_backlog.md`: `fw`/`fh` уже лежат в записи каталога (8 байт: `offset u16, fw u8, fh u8, nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = ny = 1`, то есть они -в точности равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает. +равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает. ### C7. objtable вместо отдельного fore-прохода на персонажа -Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг, браться только -если после C1–C5 бюджет всё ещё не выполняется. Оригинал кладёт персонажей и -куски в `objtable` и рисует их при обходе тайлов -(`draw_objtable_items_at_tile`), а порядок окклюзии получается сам; у нас +Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг. Оригинал +кладёт персонажей и куски в `objtable` и рисует их при обходе тайлов +(`draw_objtable_items_at_tile`), порядок окклюзии получается сам; у нас отдельный fore-проход НА КАЖДОГО персонажа. +### Снять временную оснастку + +Шесть вызовов `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит +(`call` + `ret` × 6), плюс `pop_dbg_kind`/`m16` в `pop_redraw_needed`. +Снимать ПОСЛЕ того, как оптимизация закончена: без них не мерить. + --- -## 3. Что НЕ делать +## 4. Что НЕ делать - **Не ставить W3-скобку из кода с `--w3`** — белый экран (memory `gfx_blit_noclip_fast`). @@ -185,11 +153,18 @@ nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = n (`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены, буквальные 40 экранных пикселей срезают правый задний угол плиты (прогон 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` | diff --git a/applications/PoP/docs/perf_green_phase.md b/applications/PoP/docs/perf_green_phase.md index ccd186c..53fd81e 100644 --- a/applications/PoP/docs/perf_green_phase.md +++ b/applications/PoP/docs/perf_green_phase.md @@ -3,7 +3,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)` (строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`, @@ -11,209 +12,224 @@ **Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** -| состояние | зелёная | -|---|---:| -| покой в комнате 23 | 35 760 | -| дрожат 6 плит-потолков | 366 000 | -| **пик каскада (провалы + посадки)** | **805 000 (2,0× бюджета)** | +| состояние | было (`c312e4a`) | стало (`af189a1`) | +|---|---:|---:| +| покой в комнате 23 | 35 760 | 35 760 | +| дрожат 6 плит-потолков | 366 000 | 335 400 | +| **пик каскада** | **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_redraw_needed`** | 924 | **319 944** | **737 322** | -| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 11 964 | +| **`pop_redraw_needed`** | 924 | 288 800 | **380 568** | +| хвост (шов, смена уровня, сигналы, вспышка) | 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 | -| **`pop_loose_mob_tick`** (heal летящих кусков) | 7 416 | **178 494** | -| `check_loose_fall_on_kid` | 6 120 | 3 480 | -| хвост | 324 | 3 252 | +| вид | было | стало | чем | +|---|---:|---:|---| +| `RDA_CEIL` — дрожащая плита-потолок | 48 785 | **43 536** | G2 + G3 | +| `RDA_CEIL_GONE` — запечь колодец | 251 335 | **138 318** | **G1** (клип полосы) + G2 + G3 | +| `RD_FLOOR` — щебень на месте посадки | 198 805 | **179 914** | G2 + G3 + снятая двойная пометка | -`pop_loose_mob_tick` при шести кусках в воздухе — **≈29 700 такта на кусок**. -Это heal прошлой позиции (коридор `MOB_W × 64`) плюс `pop_cd_touch` на -коридор плюс `mob_tick_one`. +Штук за кадр: `RDA_CEIL` до 6, `RDA_CEIL_GONE` до 2, `RD_FLOOR` до 2. -### 1.3 Цена ОДНОЙ перерисовки по видам (66 + 12 + 12 замеров) +### Детальный профиль `RD_FLOOR` (179 914) — главная оставшаяся статья -| вид | штук замерено | средняя цена | сколько за кадр | Σ за кадр | -|---|---:|---:|---:|---:| -| `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_dbg_b1/b5`) и по рамкам +(`pop_bar_black`, `pop_cd_batch_begin/end`): | участок | такты | |---|---:| -| `pop_heal_off(x, 0, 64, CEIL_BAND_H=8)` | 8 491 | -| `draw_tile(-1, col)` | **39 029** | +| вход `pop_floor_bake` + `gfx_set_bank` | 2 382 | +| `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` -на 6 тайлов), то есть 15–16 тыс. тактов. -**Остальные ~23 700 такта — накладные `draw_tile`, ни одного пикселя.** - -### 1.5 `RDA_CEIL_GONE` = 251 023 и `RD_FLOOR` = 198 259 - -- `pop_ceil_bake_empty(col)` — бар + **шесть** `draw_tile` - (ряд −1 и ряд 0, колонки col−1..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` -(экранные координаты, бывают отрицательными). +Итого: **105 500 — сами блиты (реальные пиксели), 74 400 — накладные**, из +которых 27 168 контекст двух `draw_tile` и 22 776 их диспетчер. --- -## 2. Способы ускорения +## 2. Что сделано (с чем сравнивать) -По убыванию отдачи. Оценки — от замеров выше; каждая позиция сверена с -`SDLPoP/src/seg008.c` (правило проекта: механику сверять с оригиналом ДО -кодинга). +### G1. Окно клипа для точечной перерисовки — −90 000 -### 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` восстанавливают ИЗВЕСТНЫЙ -прямоугольник (тот, который сами же залили баром), а зовут полный -`draw_tile`, который честно рисует и то, что в этот прямоугольник не попадает. -У `pop_floor_bake` бар — 60×39 от `yb+26`, а тело дворцовой кладки рисуется -НИЖЕ `yb+64`, то есть **вся стена рисуется мимо цели**. +Где НЕ сработало — см. §4, отрицательные результаты. -В `pop_blit_b` уже есть дешёвый предфильтр по окну `pop_t_fclip_*` — он режет -кусок ДО `atlas_image` и `gfx_w0_map`, то есть почти даром. Достаточно -выставлять это окно на время запечки. +Побочно: пока окно стоит, `pop_blit_b` не ставит пометку «фон трогали» +(признак 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` заодно подавляет -`pop_cd_touch` (признак «это fore-проход поверх персонажа»). Для запечки это -как раз правильно — она помечает область сама, — но проверить обязательно; -если понадобится, ввести отдельный флаг «окно только для отсева». +### G2. Контекст тайла — file-scope, а не локали `draw_tile` — −55 000 -Сверка с оригиналом: у него та же идея, только через `wipetable` — -`draw_tile_wipe(height)` кладёт прямоугольник в таблицу, а `draw_table` -рисует уже с известным клипом (seg008:1368, 1373). +Порт `load_curr_and_left_tile` (seg008:0339): у оригинала это +`curr_tile`/`curr_modifier`/`draw_xh`/`draw_main_y`/`draw_bottom_y` — +переменные модуля, а не локали. -### G2. Расколоть `draw_tile` + отдельный лист для ряда −1 +Причина в кодогене: в `draw_tile` **57 вызовов**, и каждое живое через вызов +значение SDCC спиливал в стековый кадр — 26 байт кадра и **513 обращений +`-N(ix)`** (при ~46 замеренных тактах на обращение это ~23 600, что и +намерено). Стало **33 обращения**, кадра нет, банк 7 −703 Б. -**Отдача: 39 029 → 18 000–20 000 на тайл полосы, то есть −140 000 на кадр -дрожания; и она умножается на G1 (шесть вызовов в запечке).** +### G3. `blit_b_clip` — байтовый габарит + file-scope -Два шага: +Два шага, и важен порядок наблюдений: -1. **Отдельный `draw_tile_aboveroom(col)`** — дословный порт seg008:01F2. - Оригинал для ряда −1 делает РОВНО шесть вещей: `draw_tile_floorright`, - `draw_tile_anim_topright`, `draw_tile_right`, `draw_tile_bottom(1)`, - `draw_loose(1)`, `draw_tile_fore`. Ни базы, ни `draw_tile_anim`, ни - `draw_tile_anim_right`, ни `wall_pattern`, ни вычисления `rbl_*` через - `row+1 > 2`. У нас всё это отсекается ветками `if (row >= 0)` уже ВНУТРИ - общего `draw_tile` — то есть кадр IX, пролог и вся арифметика оплачиваются - всегда. -2. **Расколоть общий `draw_tile` по образцу оригинала** (seg008:01C7 — - девять узких листьев). Каждый лист получает `xh`/`dby`/`dmy` параметрами - (в регистрах), локалей у него единицы, кадра IX нет. +1. **Байтового габарита ОДНОГО НЕ ХВАТИЛО.** `sx/sy/dw/dh` → `uint8_t` + (корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а + 22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi, + всё равно больше, чем регистров у Z80. +2. **Решило вынесение из локалей** (`bc_*`): 51 обращение, кадр 22 → 12 Б. -Заодно проверить `redraw_needed_above` (seg008:02C1): оригинал делает -`draw_tile_wipe(3)` — стирает **32×3**, а мы `pop_heal_off(x,0,64,8)` = -64×8. Площадь в 5 раз больше нужной (8 491 такта), хотя основная цена там — -накладные вызова. +Клипованный блит 14 088 → 11 848. Заодно `blit_b_oversize` больше не ходит +через `blit_b_clip` (там теперь байтовый габарит) — рисует напрямую +`gfx_blit_part`; это путь под полноэкранные подложки интро/финала. -### G3. `blit_b_clip` — 22 Б кадра, 211 `(ix)` +### G4. Мелочи -**Отдача: 14 088 → ~10 500 на блит. В зелёной фазе ВСЕ блиты клипованные -(полоса у потолка выставляет `pop_t_clip_top`) — при 7–17 блитах это -−25 000 … −60 000; в циане и fore-проходе больше.** - -`dx/dy/dw/dh/sx/sy` объявлены `int`. По построению `sx/sy/dw/dh ≤ 255` -(габарит любого спрайта ≤ 56×63, см. `perf_l13_room23.md` §5) — их можно -сделать `uint8_t`, оставив 16-битными только экранные `dx/dy`. -Прецедент: тот же приём в `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`), где сейчас идёт пометка -каждый кадр отсчёта. - -Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться -после G1–G4. - -### G6. `BG-ONCE` — heal вместо чёрного бара - -Предложение пользователя, см. `roomtest/TASKS_OPEN.md#bg-once`. Сейчас -запечка = «залить чёрным + перерисовать фон», то есть двойная работа; heal по -координатам бара делает то же за один проход. Прямо дополняет G1. +- `pop_blit_b`: аргументы в file-scope (третий и дальше SDCC передаёт стеком, + каждое чтение шло через `-N(ix)`) — 76 → 11 обращений. +- `pop_loose_mob_tick`: пометки всех кусков ОДНИМ пакетом + (`pop_cd_batch_begin/end`) — было по 4 502 такта на кусок. + 176 772 → 168 600. +- Коридор heal куска — по фактической высоте СОБРАННОГО композита (было 24 + строки константой, стало 20). 168 600 → 156 762. --- -## 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 на `dby−1`). То есть плита живёт в `yb+47 .. yb+65`, а бар начинается с +`yb+26` — **21 лишняя строка сверху**. + +Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла +(орнаментная лента `stripe_id` на `dmy−27 = 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; отложенная даёт призрак плиты на месте дыры - (проверено 2026-08-13). -- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — пробовалось и - убрано: плиты стартуют со случайными задержками, в кадре дрожат разрозненные - колонки, пробег почти всегда длиной в одну. -- **Не искать проблему в W3-скобке** (`_bgi_begin`/`_bgi_end` — пять - инструкций) и не списывать разброс на прерывания (внутри размерной группы - разброс три такта). + которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры. +- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — плиты стартуют со + случайными задержками, в кадре дрожат разрозненные колонки, пробег почти + всегда длиной в одну. +- **Не ускорять передачу пикселей** — предел железа (3+3 такта на байт, + 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` | diff --git a/applications/PoP/docs/perf_l13_room23.md b/applications/PoP/docs/perf_l13_room23.md index a170307..0b3c719 100644 --- a/applications/PoP/docs/perf_l13_room23.md +++ b/applications/PoP/docs/perf_l13_room23.md @@ -100,6 +100,8 @@ verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP- ## 4. Сводка по кадрам +**Базовый замер (`c312e4a`, ДО оптимизации):** + | фаза каскада | работа | синяя | зелёная | циан | период (растр.) | |---|---:|---:|---:|---:|---:| | покой в комнате 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** | **5–6** | | максимум по секции | | 142 830 | **805 000** | **631 800** | | -Цель — каждая секция ≤ 400 000. **Синяя в норме**; зелёная 2,0× бюджета, -циан 1,6×. Логический кадр вместо 3 растровых занимает 5–6: в каскаде игра -идёт вдвое медленнее нормы. +**После оптимизации (`af189a1`, 2026-08-17):** + +| максимум по секции | работа | синяя | зелёная | циан | +|---|---:|---:|---:|---:| +| было | 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). -### Общий вывод по пиковому кадру (1 437 150) +### Общий вывод по пиковому кадру базового замера (1 437 150) | | такты | доля | |---|---:|---:|