Профиль каскада плит ур.13 к.23: три рабочих документа по фазам

Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок).  Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.

Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх.  По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.

Что нашлось (всё подтверждено зондами, не гипотезы):

  RDA_CEIL      дрожащая плита-потолок     48 658 x до 6 = 292 000
  RDA_CEIL_GONE запечь колодец            251 023 x до 2 = 619 000
  RD_FLOOR      щебень на месте посадки   198 259 x до 2 = 397 000

Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600.  У blit_b_clip — 22 Б кадра и 211 (ix).

Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.

Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.

Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60.  Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).

Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):

  docs/perf_l13_room23.md  сцена, рецепт воспроизведения, зонды, канал clog,
                           сводка по кадрам, габариты спрайтов
  docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
  docs/perf_cyan_phase.md  циан: раскладка, позиции C1..C7, журнал

Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest).  Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 12:22:21 +03:00
parent c312e4a043
commit babd40bc84
5 changed files with 612 additions and 2 deletions
+168
View File
@@ -0,0 +1,168 @@
# Сцена и метод замера: каскад плит, уровень 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. Сводка по кадрам
| фаза каскада | работа | синяя | зелёная | циан | период (растр.) |
|---|---:|---:|---:|---:|---:|
| покой в комнате 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** | |
Цель — каждая секция ≤ 400 000. **Синяя в норме**; зелёная 2,0× бюджета,
циан 1,6×. Логический кадр вместо 3 растровых занимает 5–6: в каскаде игра
идёт вдвое медленнее нормы.
Где что расходуется и как это чинить — в фазовых документах:
[зелёная](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`.