Контрольный замер перед оптимизацией (задача 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>
11 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. Сводка по кадрам
| фаза каскада | работа | синяя | зелёная | циан | период (растр.) |
|---|---|---|---|---|---|
| покой в комнате 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 |
Цель — каждая секция ≤ 400 000. Синяя в норме; зелёная 2,0× бюджета, циан 1,6×. Логический кадр вместо 3 растровых занимает 5–6: в каскаде игра идёт вдвое медленнее нормы.
Где что расходуется и как это чинить — в фазовых документах: зелёная, циан.
Общий вывод по пиковому кадру (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.