2435 кадров сцены каскада. Максимумы по секциям: работа 880 170,
синяя 159 774, зелёная 419 520, циан 380 244 — против 878 550 / 158 880 /
419 526 / 379 488 у ec1f384. Все четыре в пределах шума прогона, зелёная
совпала до 6 тактов. Распределение периода совпало кадр в кадр: 4 растра
в 24 кадрах, 5 в одном, остальные 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 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 зелёной — шум прогона.
Регресс после обхода уровней 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 (кадров в этом прогоне больше просто потому, что дольше стояли в
покое после каскада). То есть за бюджет вылезает ровно тот же кусок сцены и
ровно на столько же кадров.
Скачок циан на фиксах ПЕРВОГО уровня (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.