# Сцена и метод замера: каскад плит, уровень 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 ,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** | **5–6** | | максимум по секции | | 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-механики, а не только когда меняешь её сознательно.