Замер 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>
13 KiB
Сцена и метод замера: каскад плит, уровень 13 комната 23
Общий документ для двух фазовых: perf_green_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 зелёной — шум прогона.
Скачок циан на фиксах ПЕРВОГО уровня (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 §3 (нужен раскол draw_tile на
узкие части, как в оригинале) и в идее G8 (сузить инвалидацию соседнего тайла
до 28-пиксельной полосы).
Где что расходуется и как это чинить — в фазовых документах: зелёная, циан.
Общий вывод по пиковому кадру базового замера (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.