Профиль каскада плит ур.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>
This commit is contained in:
2026-08-17 12:22:21 +03:00
parent c312e4a043
commit babd40bc84
5 changed files with 612 additions and 2 deletions
+195
View File
@@ -0,0 +1,195 @@
# ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
[`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` |