Files
Sprinter-SDCC/applications/PoP/docs/perf_cyan_phase.md
T
snark13 babd40bc84 Профиль каскада плит ур.13 к.23: три рабочих документа по фазам
Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок).  Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.

Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх.  По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.

Что нашлось (всё подтверждено зондами, не гипотезы):

  RDA_CEIL      дрожащая плита-потолок     48 658 x до 6 = 292 000
  RDA_CEIL_GONE запечь колодец            251 023 x до 2 = 619 000
  RD_FLOOR      щебень на месте посадки   198 259 x до 2 = 397 000

Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600.  У blit_b_clip — 22 Б кадра и 211 (ix).

Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.

Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.

Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60.  Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).

Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):

  docs/perf_l13_room23.md  сцена, рецепт воспроизведения, зонды, канал clog,
                           сводка по кадрам, габариты спрайтов
  docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
  docs/perf_cyan_phase.md  циан: раскладка, позиции C1..C7, журнал

Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest).  Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:22:21 +03:00

196 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные
габариты. Парная фаза — [`perf_green_phase.md`](perf_green_phase.md).
Границы фазы в `roomtest.c`: от `PROF(6)` (строка 620) до `PROF(0)`
(строка 677). Содержимое: `pop_check_mirror`, `pop_loose_mob_draw`,
соперник, `pop_char_draw(KID)`, `pop_loose_mob_draw_over`, `pop_fore_needed`,
`pop_hp_draw`, `pop_char_fore(KID)`, `pop_cd_clear`,
`pop_room_clip_borders`.
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
| состояние | циан |
|---|---:|
| покой, Кид пропущен (`pop_char_skip_mask`) | 20 550 … 28 700 |
| Кид перерисовывается, кусков нет | 107 000 … 193 000 |
| **пик каскада (6 кусков в воздухе + Кид)** | **632 000 (1,6× бюджета)** |
---
## 1. Раскладка (замер 2026-08-17, `c312e4a`)
### 1.1 Подфазы (три готовых `PROF(6)`: 0x4BCE / 0x4C26 / 0x4C93)
| участок | покой | пик (кадр 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 |
### 1.2 Блиты кусков
Блитов в циане — **19 на кадр**: шесть кусков × три части спрайта
(`env 74` = 32×3, `env 70` = 32×13, `env 72` = 26×16, порт `draw_mob`
seg007:13E5 / индексы `loose_fram_*[10]`).
| | такты |
|---|---:|
| Σ 19 блитов | **313 758** |
| на вызов | **16 273** |
То есть **половина фазы — это девятнадцать вызовов `pop_blit_b`.**
### 1.3 Внутри одного `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 %** |
| `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`).
---
## 2. Способы ускорения
По убыванию отдачи.
### 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`) частный
случай тривиален.
### 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` их читает и выбрасывает.
### C7. objtable вместо отдельного fore-прохода на персонажа
Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг, браться только
если после C1–C5 бюджет всё ещё не выполняется. Оригинал кладёт персонажей и
куски в `objtable` и рисует их при обходе тайлов
(`draw_objtable_items_at_tile`), а порядок окклюзии получается сам; у нас
отдельный fore-проход НА КАЖДОГО персонажа.
---
## 3. Что НЕ делать
- **Не ставить W3-скобку из кода с `--w3`** — белый экран
(memory `gfx_blit_noclip_fast`).
- **Не ускорять передачу пикселей** — предел железа
(memory `blit_cost_model`).
- **Не возвращать клип куска по `clip.right = 40`** оригинала
(`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены,
буквальные 40 экранных пикселей срезают правый задний угол плиты
(прогон 2026-08-13).
---
## 4. Журнал правок
| дата | что сделано | циан: покой / Кид / пик | коммит |
|---|---|---|---|
| 2026-08-17 | базовый замер (до правок этой серии) | 20 550 / 193 000 / **632 000** | `c312e4a` |