Профиль каскада плит ур.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
+219
View File
@@ -0,0 +1,219 @@
# ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
[`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` |