39c3247532
Максимумы по секциям против эталона 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>
272 lines
19 KiB
Markdown
272 lines
19 KiB
Markdown
# Сцена и метод замера: каскад плит, уровень 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** | **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-механики, а не только когда меняешь её
|
||
сознательно.
|