Files
Sprinter-SDCC/applications/PoP/docs/perf_l13_room23.md
T
snark13 39c3247532 Док 13/23: регресс после дня оптимизации 11/15
Максимумы по секциям против эталона mob-order-B-done: работа 911 862
(-1 986), синяя 149 106 (-10 704), зелёная 436 494 (-3 924), циан 382 770
(-10 230).  Период 3 растра в 2341 кадре, 4 в 23, 5 в 2 — как в эталоне.

Записано, почему сумма минусов по фазам не равна минусу по работе:
максимумы разных фаз достигаются в разных кадрах, а «работа» — максимум
суммы, а не сумма максимумов (вопрос пользователя).

Отмечено, что зелёная по-прежнему выше растрового кадра и главный
оставшийся кандидат для этой сцены — P9 (G8): сосед падающей плиты
перезапекается целиком и повторно, при шести плитах это умножается.

И записан урок процесса: прогон 13/23 обязателен после каждой правки
loose-механики — именно он вскрыл пропущенный взвод гейта в check_fall_flo,
которого не поймали ни хост-тесты, ни сцена 11/15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:56:07 +03:00

272 lines
19 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.
# Сцена и метод замера: каскад плит, уровень 13 комната 23
Общий документ для двух фазовых: [`perf_green_phase.md`](perf_green_phase.md)
(слой фона) и [`perf_cyan_phase.md`](perf_cyan_phase.md) (персонажи + передний
слой). Здесь — как воспроизвести сцену, чем мерить, сводка по кадрам и
разбор габаритов спрайтов (он общий для обеих фаз).
Сцена: старт уровня 13. Комната 23 стартовая, ряд 2 комнаты СВЕРХУ (17) —
шесть loose-плит в колонках 2..7 (`res2013.bin`: коды `11` в позициях 22..27),
`check_fall_flo` раздаёт им отложенный старт `0xF0..0xFF`, и они сыплются
вразнобой. Кид стоит у правого края и не двигается.
Все числа — такты `totalcycles` MAME (системный клок ~21,5 МГц, **НЕ** такты
Z80: у ОЗУ Sprinter wait-state'ы, ≈2,4× номинала — memory
`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Логический кадр
спейсится тремя `gfx_wait_vsync`, поэтому работа сверх 430 000 стоит сразу
целый лишний растровый кадр.
Сборка: `make LEVEL=13` на `c312e4a`, `_CODE = 0x4100`, база модуля
`roomtest.c` = **0x42AD**. **Адреса зондов меняются после КАЖДОЙ
пересборки** — брать заново из `.sprinter-cc-roomtest/roomtest.map` и
`roomtest.lst`.
---
## 1. Как воспроизвести сцену
**Только перезапуском программы.** Проверено и отвергнуто:
- **выход из комнаты и возврат** (чит `+`/`-`) — не работает: провалившаяся
плита-потолок уходит в страницу уровня насовсем (`pop_level_set_tile` в
`roomtest.c` по сигналу `pop_ceil_fell`, плюс `animate_loose` в
`pop_trob.c`), и при повторном входе `check_fall_flo` не находит ни одной
`TILE_LOOSE`;
- **рестарт уровня** (`pop_kid_dead = 1` + чит навигации) — не работает по
другой причине: `pop_start_level()` сам заходит в стартовую комнату 23,
взводит гряду, и она доваливается ЗАОЧНО (через `trob` комнаты 17), пока
телепорт уносит Кида в комнату 24;
- **поставить сцену руками** (записать `pop_ceil_modif[2..7]` и копию ряда
сверху `pop_t_above[2..7]` отладчиком) — записи ложатся, но пока машина
БЕЖИТ, их успевает обнулить тот же доваливающийся `trob`.
Рабочий рецепт (идея пользователя, самый чистый): **`ESC` → зонды →
`roomtest`**. `ESC` выходит в DSS, запуск заново стартует уровень 13 с нуля,
Кид сразу в комнате 23, каскад начинается через ~5 логических кадров после
отрисовки комнаты. Зонды обязаны стоять **ДО** набора `roomtest` — за время
набора (9 клавиш ≈ 1,8 с) и загрузки атласов каскад успевает пройти целиком.
## 2. Канал вывода замеров
`printf` из действия брейкпоинта в `error.log` **не** попадает. Читается
verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP-обёртке
(`mame_mcp.py` знает только `cmd`). Годится прямой файловый IPC:
положить `/tmp/mame_mcp/req_<ЧИСЛО>.txt` с телом команды и прочитать
`resp_<ЧИСЛО>.txt`. **Имя обязано содержать ЧИСЛО** (`init.lua`:
`entry:match("^req_(%d+)%.txt$")`) — с буквенным id запрос молча не
обслуживается.
Скрипты сессии (в scratchpad, при необходимости пересоздать): `mrpc.py`
клиент IPC; `run.sh` — цикл «`bpclear``ESC` → зонды → `roomtest``clog`»;
`parse*.py` — разбор трассы по кадрам.
Форма зонда: `bpset <addr>,1,{printf "<метка> %d",totalcycles; g}`.
Для `pop_dbg_kind``printf "K %d %d",a,totalcycles` (аргумент `uint8_t`
приходит в `A`, `__sdcccall(1)`).
## 3. Зонды
Адреса `out (_io_border), a` (полосы бордюра) из `roomtest.lst` плюс
однобайтовые пустышки `pop_dbg_*` из резидентного `pop_state.c`. Резидент
важен принципиально: у банковых функций один адрес 0xC000+ есть у восьми
модулей сразу, и брейкпоинт ловит все банки (так в прошлой сессии намерили
несуществующие 134 730 тактов).
| зонд | адрес | что |
|---|---|---|
| A | 0x43E8 | `PROF(2)` — начало кадра (ввод + heal) |
| — | 0x46AF | `PROF(2)` — начало логики |
| C | 0x47D1 | `PROF(4)` — начало слоя фона (**зелёная**) |
| D | 0x4BCE | `PROF(6)` — начало спрайтов (**циан**) |
| M | 0x4C26 | `PROF(6)` — кадр Кида |
| F | 0x4C93 | `PROF(6)` — fore поверх Кида |
| E | 0x4CB7 | `PROF(0)` — конец работы, ждём vsync |
| m5/m6/m7 | 0x4DC6 / C7 / C8 | границы внутри зелёной |
| m9..m12 | 0x4DCA..CD | внутренности `pop_loose_tick` |
| m13/m14/m15 | 0x4DCE / CF / D0 | `pop_ceil_shake_draw`: вход / heal / draw_tile |
| kind / m16 | 0x4DD1 / D2 | вид и цена одной перерисовки в `pop_redraw_needed` |
| b1..b5 | 0x4DD3..D7 | участки одного `pop_blit_b` |
Полезные адреса состояния (из `roomtest.map`): `pop_t_room` 0x95F2,
`pop_current_level` 0x9945, `pop_ceil_modif` 0x9C9E, `pop_t_above` 0x95EE
(указатель), `pop_kid_dead` 0x9C4E, `pop_dbg_rdmax` 0x95B1.
Запись в память через MCP — по адресу `0x10000 | addr` (логический вид Z80);
присваивание выражением дебаггера (`print b@... = 1`) **не работает**.
**Грабли:** проверять, что запущен РОВНО ОДИН MAME (`pgrep -f mame.arm | wc -l`).
Мост говорит с одним, замеры собираются с другого, и точки «не срабатывают».
---
## 4. Сводка по кадрам
**Базовый замер (`c312e4a`, ДО оптимизации):**
| фаза каскада | работа | синяя | зелёная | циан | период (растр.) |
|---|---:|---:|---:|---:|---:|
| покой в комнате 23 | 190 860 | 134 550 | 35 760 | 20 550 | **3** |
| дрожат 6 плит | 537 400 | 142 700 | 366 000 | 28 700 | **4** |
| провалы + полёт, ПИК | **1 437 150** | 142 700 | **663 250** | **631 200** | **56** |
| максимум по секции | | 142 830 | **805 000** | **631 800** | |
**После оптимизации (`af189a1`, 2026-08-17):**
| максимум по секции | работа | синяя | зелёная | циан |
|---|---:|---:|---:|---:|
| было | 1 437 150 | 142 830 | 805 000 | 631 800 |
| стало | **916 458** | 142 830 | **546 900** | **270 510** |
| | 36 % | — | 32 % | 57 % |
**Регресс после обхода уровней 1-2 (`ec1f384`, 2026-08-17), 418 кадров:**
| максимум по секции | работа | синяя | зелёная | циан |
|---|---:|---:|---:|---:|
| после оптимизации (`af189a1`) | 916 458 | 142 830 | 546 900 | 270 510 |
| после фиксов ур. 1 (`40f0d46`) | 873 930 | 158 874 | 417 630 | 379 482 |
| **после фиксов ур. 2 (`ec1f384`)** | **878 550** | **158 880** | **419 526** | **379 488** |
Фиксы второго уровня (чёрные бары, чит бессмертия) на бюджет не повлияли:
разница с предыдущим замером +4 620 работы и +1 896 зелёной — шум прогона.
**Регресс после обхода уровней 3-7 (`3bcaf51`, 2026-08-18), 2435 кадров:**
| максимум по секции | работа | синяя | зелёная | циан |
|---|---:|---:|---:|---:|
| после фиксов ур. 2 (`ec1f384`) | 878 550 | 158 880 | 419 526 | 379 488 |
| **после фиксов ур. 3-7 (`3bcaf51`)** | **880 170** | **159 774** | **419 520** | **380 244** |
| разница | +1 620 | +894 | 6 | +756 |
| | +0,2 % | +0,6 % | 0,0 % | +0,2 % |
Все четыре секции — в пределах шума прогона (сравнить с +4 620 / +1 896
выше, которые уже признаны шумом). Зелёная совпала с точностью до 6 тактов.
Что за это время добавилось в горячий путь: `pop_spike_frame` и
`pop_chomp_pose` (SPIKE-BAKED) — один резидентный `call` на слой и ТОЛЬКО на
тайлах-ловушках, в этой комнате их нет; и снятие раннего выхода для трупа
(DIED-ON-BUTTON) — цепочка физики на мёртвом Киде, а он тут жив. Замер это
подтверждает: цена не сдвинулась.
Распределение периода тоже совпало с эталоном кадр в кадр: **3 растра в
2408 кадрах, 4 в 24, 5 в одном** — против «3 в 392, 4 в 24, 5 в одном»
у `ec1f384` (кадров в этом прогоне больше просто потому, что дольше стояли в
покое после каскада). То есть за бюджет вылезает ровно тот же кусок сцены и
ровно на столько же кадров.
**Регресс после обхода уровней 8-9 (`0dd2f6a`, 2026-08-18), 2701 кадр:**
| максимум по секции | работа | синяя | зелёная | циан |
|---|---:|---:|---:|---:|
| после фиксов ур. 3-7 (`3bcaf51`) | 880 170 | 159 774 | 419 520 | 380 244 |
| **после фиксов ур. 8-9** | **880 272** | **159 822** | **419 562** | **380 202** |
| разница | +102 | +48 | +42 | 42 |
Разброс ±100 тактов на 880 000 — это 0,01 %, то есть чистый шум прогона
(циан вообще ушёл в минус). Период снова совпал кадр в кадр: 4 растра в
24 кадрах, 5 в одном.
Что добавилось за это время и почему не подорожало: фиксы стража
(`c40ae3f`) правят только вход в комнату — кода в кадре не прибавилось; фикс
боя у шва (`a498255`) добавил два сравнения в `check_leave`, а в этой сцене
Кид неподвижен и до порогов не доходит. Отладочная трасса `DBG_KIDOBJ`
выключена дефайном и в сборку не попадает.
Скачок циан на фиксах ПЕРВОГО уровня (270 510 → 379 482) объяснён там же:
восстановлены потерянные половины слоёв (`set_redraw2`, ряд 1 foretable), то
есть это плата за корректность, а не регрессия.
Период кадра по прогону: **3 растра в 392 кадрах, 4 в 24, 5 в одном** — то
есть за бюджет вылезает только сам каскад.
Цель — каждая секция ≤ 400 000. **Синяя и циан в бюджете**; зелёная 419 526,
то есть 1,05× цели (и ниже растрового кадра 430 000), остаток разобран в
[`perf_green_phase.md`](perf_green_phase.md) §3 (нужен раскол `draw_tile` на
узкие части, как в оригинале) и в идее G8 (сузить инвалидацию соседнего тайла
до 28-пиксельной полосы).
Где что расходуется и как это чинить — в фазовых документах:
[зелёная](perf_green_phase.md), [циан](perf_cyan_phase.md).
### Общий вывод по пиковому кадру базового замера (1 437 150)
| | такты | доля |
|---|---:|---:|
| блиты (все 27–28 вызовов `pop_blit_b`) | 488 100 | 34 % |
| из них «железный» минимум пикселей (модель `198*h + 5,96*w*h`) | ~150 000 | 10 % |
| синяя (ввод + heal + логика) | 142 700 | 10 % |
| **наши накладные: `draw_tile`, IX-кадры, диспетчер, пометки** | **~1 150 000** | **~80 %** |
Узкое место — НЕ передача пикселей (она на пределе железа, 3+3 такта на байт,
memory `blit_cost_model`), а 16-битная арифметика в стековых кадрах.
---
## 5. Габариты спрайтов: можно ли всё перевести на `uint8_t`
Просканированы каталоги ВСЕХ `.atl` (109 файлов) и исходные PNG наборов
`TITLE`/`PV` — тех, что понадобятся для интро, финала и роликов между
уровнями.
**Игровой кадр — весь укладывается в байт:**
| набор | максимум |
|---|---|
| фон подземелья/дворца (`*_env*`, `*_wall`, `*_fore`, `pop_pot`) | **48 × 63** |
| Кид (`kid0..27`, `sword`) | **56 × 63** (kid3, idx 0) |
| страж / скелет / Джафар | **53 × 42** |
| спрайты комнаты принцессы (`PV.DAT`: персонажи, песочные часы, факел, звёзды) | **49 × 60** |
**Больше 255 — только полноэкранные подложки титров и сюжетных экранов.**
Их восемь, и все рисуются ОДИН раз при показе экрана:
| ресурс | размер | где (`data.h`, `full_image[]`) |
|---|---|---|
| `TITLE/res51` | 320 × 200 | `TITLE_MAIN`, xpos 0 ypos 0 |
| `TITLE/res41` | 320 × 200 | `STORY_FRAME`, xpos 0 ypos 0 |
| `PV/res951` | 320 × 200 | фон комнаты принцессы (`chtab_9_princessbed`) |
| `TITLE/res42..res45` | 272 / 267 / 264 / **256** × 134..142 | «presents», «Prince of Persia», «Mechner» |
| `TITLE/res54` | 272 × 65 | заголовок Hall of Fame |
Высота нигде не превышает 200 — в байт лезет. По ширине не лезут ровно эти
восемь, и ни одна из них не участвует в игровом кадре.
**Вывод: горячий путь можно переводить на 8-битные габариты целиком.**
Для подложек — решение пользователя (2026-08-17): работу с роликами вынести в
отдельный банк с версиями блита под большие спрайты либо звать libbgi напрямую
— клип и проверка выхода за экран им не нужны (рисуются в x = 0/24/48/96,
заведомо внутри 320×200). Ширина 320 всё равно потребует ДВУХ burst-скобок
акселератора на строку — как уже сделано в `pop_vflip`.
Существующая страховка уже есть и остаётся: `pop_blit_b` уводит кадр с
`img[1] | img[3] != 0` на общий путь `blit_b_oversize`.
**Регресс после дня оптимизации 11/15 (`d0ac4b1`, 2026-08-19), 2367 кадров:**
| максимум по секции | эталон `mob-order-B-done` | сейчас | разница |
|---|---:|---:|---:|
| работа | 913 848 | **911 862** | 1 986 |
| синяя | 159 810 | **149 106** | 10 704 |
| зелёная | 440 418 | **436 494** | 3 924 |
| циан | 393 000 | **382 770** | 10 230 |
Период: **3 растра в 2341 кадре, 4 в 23, 5 в 2** — как в эталоне.
Почему сумма минусов по фазам не равна минусу по работе: максимумы разных
фаз достигаются В РАЗНЫХ КАДРАХ (пик синей — не тот кадр, где пик зелёной),
а «работа» здесь — максимум СУММЫ, а не сумма максимумов.
Что из правок 11/15 сюда дошло: P16 и P2b дали синюю и циан (они про
проверки и луч видимости, а те работают в любой сцене), HEAL-WIDTH дал
зелёную (плита 64 → 58 на шести heal'ах кадра).
**Зелёная по-прежнему выше растрового кадра** (436 494 против 430 000).
Главный оставшийся кандидат именно для этой сцены — **P9 (G8)**: при
падении плиты помечаются ДВА тайла, и соседний перезапекается целиком и
повторно (`draw_tile` соседа дважды на одну пометку), хотя потревожены у
него только левые 28 пикселей. При шести падающих плитах это умножается
на шесть.
**ВАЖНО ДЛЯ ПРОЦЕССА.** Этот прогон вскрыл регрессию, которую не поймали
ни хост-тесты, ни сцена 11/15: гейт `loose_any` (позиция P5) не взводился
в `check_fall_flo`, и плиты уровня 13 дрожали, не падая. Сцену 13/23 надо
прогонять после КАЖДОЙ правки loose-механики, а не только когда меняешь её
сознательно.