Документы по фазам: результаты дня и оставшийся план

Зелёная и циан — раскладка ПОСЛЕ правок, журнал правок с коммитами,
отрицательные результаты с объяснением причины (окно клипа в
pop_floor_bake — три попытки), и что осталось: раскол draw_tile на девять
узких частей по образцу seg008:01C7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:23:38 +03:00
parent af189a1d92
commit 14e183108d
3 changed files with 294 additions and 293 deletions
+177 -161
View File
@@ -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, колонки 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`
(экранные координаты, бывают отрицательными).
Итого: **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 00020 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`), где сейчас идёт пометка
каждый кадр отсчёта.
Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться
после G1G4.
### 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 на `dby1`). То есть плита живёт в `yb+47 .. yb+65`, а бар начинается с
`yb+26`**21 лишняя строка сверху**.
Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла
(орнаментная лента `stripe_id` на `dmy27 = 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` |