Files
Sprinter-SDCC/applications/PoP/docs/perf_green_phase.md
T
snark13 f44663040c Регресс тактов на 23/13 после обхода уровней 1-2: изменений нет
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).

                работа     синяя   зелёная      циан
af189a1        916 458   142 830   546 900   270 510
40f0d46        873 930   158 874   417 630   379 482
ec1f384        878 550   158 880   419 526   379 488

Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона.  Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.

Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000.  Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).

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

328 lines
23 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).**
| состояние | было (`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. Раскладка ПОСЛЕ правок (замер `af189a1`)
Пик — кадры 24-28 (посадки плит), больше не кадры провалов.
| участок | покой | дрожь | пик |
|---|---:|---:|---:|
| `pop_loose_tick` | 36 234 | 36 234 | **156 762** |
| `pop_process_trobs` | 1 050 | 1 050 | 1 050 |
| **`pop_redraw_needed`** | 924 | 288 800 | **380 568** |
| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 10 776 |
`pop_loose_tick` изнутри на пике: два цикла по тайлам 9 852,
**`pop_loose_mob_tick` 154 074** (heal шести летящих кусков), остальное мелочь.
### Цена одной перерисовки: было → стало
| вид | было | стало | чем |
|---|---:|---:|---|
| `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 + снятая двойная пометка |
Штук за кадр: `RDA_CEIL` до 6, `RDA_CEIL_GONE` до 2, `RD_FLOOR` до 2.
### Детальный профиль `RD_FLOOR` (179 914) — главная оставшаяся статья
Снят зондами по каждому блиту (`pop_dbg_b1/b5`) и по рамкам
(`pop_bar_black`, `pop_cd_batch_begin/end`):
| участок | такты |
|---|---:|
| вход `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 |
Итого: **105 500 — сами блиты (реальные пиксели), 74 400 — накладные**, из
которых 27 168 контекст двух `draw_tile` и 22 776 их диспетчер.
---
## 2. Что сделано (с чем сравнивать)
### G1. Окно клипа для точечной перерисовки — −90 000
`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`
режет остальные по нему.
Где сработало: `pop_ceil_bake_empty` — куски ряда 0 высотой 63 px рисовались
целиком, хотя восстановить надо девять строк полосы. **Блит 21 447 → 7 619**,
вся перерисовка 251 335 → 138 318.
Где НЕ сработало — см. §4, отрицательные результаты.
Побочно: пока окно стоит, `pop_blit_b` не ставит пометку «фон трогали»
(признак fore-прохода), поэтому вызывающий обязан пометить прямоугольник сам.
В `pop_ceil_shake_draw` добавлен явный `pop_cd_touch` на область heal'а; в
`pop_ceil_bake_empty` и `pop_floor_bake` метит `pop_bar_black`, а лишний
второй вызов на ту же область снят.
### G2. Контекст тайла — file-scope, а не локали `draw_tile` — 55 000
Порт `load_curr_and_left_tile` (seg008:0339): у оригинала это
`curr_tile`/`curr_modifier`/`draw_xh`/`draw_main_y`/`draw_bottom_y`
переменные модуля, а не локали.
Причина в кодогене: в `draw_tile` **57 вызовов**, и каждое живое через вызов
значение SDCC спиливал в стековый кадр — 26 байт кадра и **513 обращений
`-N(ix)`** (при ~46 замеренных тактах на обращение это ~23 600, что и
намерено). Стало **33 обращения**, кадра нет, банк 7 −703 Б.
### G3. `blit_b_clip` — байтовый габарит + file-scope
Два шага, и важен порядок наблюдений:
1. **Байтового габарита ОДНОГО НЕ ХВАТИЛО.** `sx/sy/dw/dh``uint8_t`
(корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а
22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi,
всё равно больше, чем регистров у Z80.
2. **Решило вынесение из локалей** (`bc_*`): 51 обращение, кадр 22 → 12 Б.
Клипованный блит 14 088 → 11 848. Заодно `blit_b_oversize` больше не ходит
через `blit_b_clip` (там теперь байтовый габарит) — рисует напрямую
`gfx_blit_part`; это путь под полноэкранные подложки интро/финала.
### G4. Мелочи
- `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. Что осталось: раскол `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. Пометка соседа — узкой полосой, а не полным тайлом (идея пользователя)
**Ожидание: заметное, но не мерено. Взять ПОСЛЕ обхода всех уровней**
(решение пользователя 2026-08-17: пока идёт отлов багов слоёв, каждая правка
добавляет переменных в картину).
Когда плита (1,8) падает, помечаются ДВА тайла:
| пометка | что делает |
|---|---|
| `(1,8)``RD_LOOSE_GONE` | бар 40 на своём x, бар 32 на соседе, `draw_tile(1,8)` + `draw_tile(1,9)` |
| `(1,9)``RD_FLOOR` | бар **60** на x соседа, `draw_tile(1,9)` ЕЩЁ РАЗ |
То есть сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО, хотя потревожили у него
только левые 28 пикселей — там, куда свисает правая грань упавшего тайла.
`draw_tile(1,9)` при этом вызывается дважды на одну пометку.
Что такое эти числа (чтобы не сузить лишнего):
- **60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес.** Правая грань пола (кадр 42,
26 px) рисуется в клетке соседа с `x+32`, занимая `x+32..x+57`. Для запечки
САМОГО тайла 60 уже минимальны — сужать их нельзя;
- сузить можно только тот случай, когда тайл помечен ПОТОМУ ЧТО ИЗМЕНИЛСЯ ЕГО
ЛЕВЫЙ СОСЕД: тогда нужна полоса 28 px у левого края, а не весь тайл.
Почему выигрыш не символический: `pop_floor_bake` стоит **179 914** тактов,
из них 105 500 — сами блиты. Узкая полоса срезала бы и площадь бара
(60×39 → 28×39), и часть блитов — окно клипа там теперь стоит обязательным
(см. §4), так что отсев достаётся даром.
**Условия, из-за которых это не «просто уменьшить число»:**
1. `pop_floor_bake` — ОБЩАЯ функция: её же зовут кнопка (`pop_button_redraw`),
зеркало, подобранный предмет и щебень на месте посадки. Там меняется сам
тайл и 60 нужны целиком. Значит нужен отдельный вход (напр.
`pop_floor_bake_edge(row, col)`) или параметр-прямоугольник — именно под
пометку «изменился мой левый сосед».
2. Прежде чем выкидывать вторую пометку целиком, сверить ВЕРТИКАЛЬНЫЕ
диапазоны: `pop_loose_bake_empty` кроет `63*row+46 .. +65` (20 строк), а
`pop_floor_bake``yb+26 .. yb+64` (39 строк). То есть сосед покрыт
ВТОРЫМ баром не полностью, и просто снять пометку нельзя.
3. Ширина полосы = свес ЛЕВОГО тайла, а он зависит от типа тайла (у loose это
8 px по комментарию в `pop_loose_bake_empty`, у пола 26). Брать по
максимуму (28) — безопасно.
### G9. Снять временную оснастку
Шесть `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит; при 15
блитах зелёной это 6 000 на кадр. Плюс `pop_dbg_kind`/`m16` (2 вызова на
перерисовку) и `pop_dbg_m5..m15`. Снимать ПОСЛЕ окончания оптимизации: без
них не мерить.
---
## 4. Копия второй страницы — и почему она ТРЕБУЕТ окна клипа
Точечные запечки ставятся с `pages = 2`, срабатывают два кадра подряд (по разу
на страницу дабл-буфера) и оба раза считают одно и то же. После первого раза
нужный прямоугольник уже лежит в ОЗУ-копии первой страницы, и его можно
скопировать: `gfx_copy_page` берёт источником ОЗУ-копию НЕактивной страницы
(то есть ЧИСТЫЙ фон — спрайты рисуются банком без тени и в копию не попадают),
а приёмник обновляет и в видео-ОЗУ, и в ОЗУ-копии. Идея пользователя: тот же
приём, что при перевороте экрана (зелёное зелье), только без зеркала.
| | полная запечка | копия |
|---|---:|---:|
| щебень / кнопка (60×39) | 179 914 | ~35 000 |
| колодец полосы потолка (64×9) | 138 318 | ~17 500 |
**ДВА УСЛОВИЯ КОРРЕКТНОСТИ.** Оба нарушались и оба дали видимые баги.
1. **Запечка обязана быть ОГРАНИЧЕНА копируемым прямоугольником.**
`draw_tile` рисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара; копия переносит
ровно бар, и всё, что легло вне него, на второй странице остаётся прежним —
страницы расходятся, это видно как МЕРЦАНИЕ через кадр. У полосы потолка
окно стояло с самого начала (G1), у `pop_floor_bake` — нет, и он мерцал
торцами полов, плит и кнопок (найдено пользователем 2026-08-17: уровень 1,
комната 6, Кид на кнопке (0,2)). Поэтому в `pop_floor_bake` окно теперь
стоит КАК УСЛОВИЕ КОРРЕКТНОСТИ, хотя по скорости само по себе убыточно
(см. §5) — снимать его нельзя.
2. **Копия годится только если содержимое тайла между двумя кадрами не
изменилось.** У анимированного тайла (кнопка с идущим таймером связи)
пометка обновляется КАЖДЫЙ кадр и картинка каждый раз другая. Поэтому
`pop_set_redraw`/`pop_set_redraw_above` гасят слот копии при ПЕРЕпометке
(`pop_bake_slot_reset*`).
Плюс слот `bake_pg`/`bake_pg_above` помнит, НА КАКОЙ странице сделана первая
запечка: копируем только если первая была на ДРУГОЙ странице и дабл-буфер
включён. Это покрывает переплетение двух запечек в одном кадре, однобуфер
(чит SPACE) и смену комнаты (`pop_bake_forget`).
---
## 5. Отрицательные результаты — НЕ повторять
### Окно клипа в `pop_floor_bake` — по СКОРОСТИ проверено ТРИ раза, каждый раз хуже
**Но оно всё равно стоит на месте: без него ломается копия второй страницы
(§4).** Ниже — только про скорость самого окна.
| попытка | было | стало |
|---|---:|---:|
| до 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 перерисовкам их нет. Запись стоит в коде.
### Прочее (проверено раньше)
- **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из
которой восстанавливает 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`.
---
## 6. Журнал правок
| дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|---|---|---|---|
| 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` |
| 2026-08-17 | **копия второй страницы вместо второй запечки** | — / — / **423 558** | `5ef721e` |
| 2026-08-17 | `mob_tick_one` в file-scope; снята оснастка из горячих путей | 35 760 / 326 130 / **414 456** | `18ee60e` |
| 2026-08-17 | фикс мерцания: окно клипа в `pop_floor_bake` как условие корректности копии | замер после фикса — ниже | `35b7cd5` |
| 2026-08-17 | замер после фиксов уровня 1 (мерцание торцов, потолочный fore, сосед под плитой, блеск меча) | 35 760 / — / **417 630** | `40f0d46` |
| 2026-08-17 | **регресс после фиксов уровня 2** (чёрные бары, чит бессмертия) — в пределах шума | — / — / **419 526** | `ec1f384` |