Files
Sprinter-SDCC/applications/PoP/docs/perf_green_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

220 lines
13 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_cyan_phase.md`](perf_cyan_phase.md).
Границы фазы в `roomtest.c`: от `PROF(4)` (строка 450) до `PROF(6)`
(строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`,
`pop_redraw_needed`, шов, смена уровня, сигналы провалов, вспышка.
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
| состояние | зелёная |
|---|---:|
| покой в комнате 23 | 35 760 |
| дрожат 6 плит-потолков | 366 000 |
| **пик каскада (провалы + посадки)** | **805 000 (2,0× бюджета)** |
---
## 1. Раскладка (замер 2026-08-17, `c312e4a`, зонды m5/m6/m7 + kind/m16)
### 1.1 Верхний уровень
| участок | покой | дрожь | пик |
|---|---:|---:|---:|
| `pop_loose_tick` | 25 602 | 35 646 | **196 152** |
| `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` — 91 % фазы на пике.**
### 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 |
`pop_loose_mob_tick` при шести кусках в воздухе — **≈29 700 такта на кусок**.
Это heal прошлой позиции (коридор `MOB_W × 64`) плюс `pop_cd_touch` на
коридор плюс `mob_tick_one`.
### 1.3 Цена ОДНОЙ перерисовки по видам (66 + 12 + 12 замеров)
| вид | штук замерено | средняя цена | сколько за кадр | Σ за кадр |
|---|---:|---:|---:|---:|
| `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 |
| `draw_tile(-1, col)` | **39 029** |
Блитов внутри этого `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, колонки 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. Способы ускорения
По убыванию отдачи. Оценки — от замеров выше; каждая позиция сверена с
`SDLPoP/src/seg008.c` (правило проекта: механику сверять с оригиналом ДО
кодинга).
### G1. Окно клипа для ЗАПЕЧКИ — самый крупный кусок
**Отдача: 250 000 … −400 000 на пиковый кадр.**
`pop_ceil_bake_empty` и `pop_floor_bake` восстанавливают ИЗВЕСТНЫЙ
прямоугольник (тот, который сами же залили баром), а зовут полный
`draw_tile`, который честно рисует и то, что в этот прямоугольник не попадает.
У `pop_floor_bake` бар — 60×39 от `yb+26`, а тело дворцовой кладки рисуется
НИЖЕ `yb+64`, то есть **вся стена рисуется мимо цели**.
В `pop_blit_b` уже есть дешёвый предфильтр по окну `pop_t_fclip_*` — он режет
кусок ДО `atlas_image` и `gfx_w0_map`, то есть почти даром. Достаточно
выставлять это окно на время запечки.
Грабли, которые надо обойти: `pop_t_fclip_on` в `pop_blit_b` заодно подавляет
`pop_cd_touch` (признак «это fore-проход поверх персонажа»). Для запечки это
как раз правильно — она помечает область сама, — но проверить обязательно;
если понадобится, ввести отдельный флаг «окно только для отсева».
Сверка с оригиналом: у него та же идея, только через `wipetable`
`draw_tile_wipe(height)` кладёт прямоугольник в таблицу, а `draw_table`
рисует уже с известным клипом (seg008:1368, 1373).
### G2. Расколоть `draw_tile` + отдельный лист для ряда −1
**Отдача: 39 029 → 18 00020 000 на тайл полосы, то есть −140 000 на кадр
дрожания; и она умножается на G1 (шесть вызовов в запечке).**
Два шага:
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 нет.
Заодно проверить `redraw_needed_above` (seg008:02C1): оригинал делает
`draw_tile_wipe(3)` — стирает **32×3**, а мы `pop_heal_off(x,0,64,8)` =
64×8. Площадь в 5 раз больше нужной (8 491 такта), хотя основная цена там —
накладные вызова.
### G3. `blit_b_clip` — 22 Б кадра, 211 `(ix)`
**Отдача: 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`), где сейчас идёт пометка
каждый кадр отсчёта.
Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться
после G1G4.
### G6. `BG-ONCE` — heal вместо чёрного бара
Предложение пользователя, см. `roomtest/TASKS_OPEN.md#bg-once`. Сейчас
запечка = «залить чёрным + перерисовать фон», то есть двойная работа; heal по
координатам бара делает то же за один проход. Прямо дополняет G1.
---
## 3. Что НЕ делать (проверено, отрицательный результат)
- **Не ускорять передачу пикселей** — она на пределе железа (3+3 такта на
байт через акселератор), memory `blit_cost_model`. В пиковом кадре
«железный» минимум всех блитов ≈150 000 из 1 437 150.
- **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из
которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры
(проверено 2026-08-13).
- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — пробовалось и
убрано: плиты стартуют со случайными задержками, в кадре дрожат разрозненные
колонки, пробег почти всегда длиной в одну.
- **Не искать проблему в W3-скобке** (`_bgi_begin`/`_bgi_end` — пять
инструкций) и не списывать разброс на прерывания (внутри размерной группы
разброс три такта).
---
## 4. Журнал правок
| дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|---|---|---|---|
| 2026-08-17 | базовый замер (до правок этой серии) | 35 760 / 366 000 / **805 000** | `c312e4a` |