Files
Sprinter-SDCC/applications/PoP/docs/perf_cyan_phase.md
T
snark13 babd40bc84 Профиль каскада плит ур.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>
2026-08-17 12:22:21 +03:00

11 KiB
Raw Blame History

ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация

Рабочий документ: живёт между сессиями. Внизу журнал правок — каждая запись с замером до/после. Сцена, рецепт воспроизведения и зонды — perf_l13_room23.md; там же ответ про 8-битные габариты. Парная фаза — perf_green_phase.md.

Границы фазы в roomtest.c: от PROF(6) (строка 620) до PROF(0) (строка 677). Содержимое: pop_check_mirror, pop_loose_mob_draw, соперник, pop_char_draw(KID), pop_loose_mob_draw_over, pop_fore_needed, pop_hp_draw, pop_char_fore(KID), pop_cd_clear, pop_room_clip_borders.

Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).

состояние циан
покой, Кид пропущен (pop_char_skip_mask) 20 550 … 28 700
Кид перерисовывается, кусков нет 107 000 … 193 000
пик каскада (6 кусков в воздухе + Кид) 632 000 (1,6× бюджета)

1. Раскладка (замер 2026-08-17, c312e4a)

1.1 Подфазы (три готовых PROF(6): 0x4BCE / 0x4C26 / 0x4C93)

участок покой пик (кадр 20) максимум
pop_check_mirror + pop_loose_mob_draw (куски ПОД Кидом) + соперник 23 250 34 950 154 512
pop_char_draw(KID) + pop_loose_mob_draw_over + pop_fore_needed + HP 3 726 445 284 445 284
pop_char_fore(KID) + pop_cd_clear + чистка бортов 1 722 150 978 150 978

1.2 Блиты кусков

Блитов в циане — 19 на кадр: шесть кусков × три части спрайта (env 74 = 32×3, env 70 = 32×13, env 72 = 26×16, порт draw_mob seg007:13E5 / индексы loose_fram_*[10]).

такты
Σ 19 блитов 313 758
на вызов 16 273

То есть половина фазы — это девятнадцать вызовов pop_blit_b.

1.3 Внутри одного pop_blit_b (157 замеров быстрого пути, зонды b1..b5)

участок такты доля
atlas_image + gfx_w0_map + чтение габарита 1 086 7 %
ядро блита (libbgi, gfx_blit_noclip) 10 422 64 %
pop_cd_touch — пометка «фон тронут» 4 502 28 %
gfx_w0_unmap + возврат 264 2 %
ИТОГО 16 273

Для сравнения, клипованный путь (227 замеров, полоса у потолка): ядро blit_b_clip 14 088, итого на вызов 16 409.

По модели blit_cost_model «железный» минимум передачи для этих трёх спрайтов — 5 000 … 6 200 тактов. Остальные ~10 000 на вызов — накладные.

1.4 pop_char_fore(KID) вырастает в каскаде на два порядка

В покое 1 722, в кадрах каскада 150 978. Кид стоит у правого края и НЕ двигается, плиты падают в колонках 2..7 — то есть либо пропуск персонажа (pop_char_skip_mask) гасится пометками pop_cd_touch от кусков, либо окно fore-клипа расширяется и в него попадают лишние тайлы. Не разобрано — см. C4.

1.5 Кодоген

функция стековый кадр обращений (ix) ожидаемо тактов
blit_b_clip (резидент) 22 Б 211 ~9 700
pop_blit_b (резидент) 6 Б 78 ~3 600
pop_cd_touch (резидент) 30 ~1 380
mob_draw_pass (bank 7) 18 Б 20 ~900
mob_render (bank 7) 1

Одно -N(ix) ≈ 19 номинальных тактов ≈ 46 замеренных (memory sprinter_wait_states_2x).


2. Способы ускорения

По убыванию отдачи.

C1. Убрать избыточный pop_cd_touch в mob_render

Отдача: 81 000 (13 % фазы). Самая дешёвая правка из всех.

pop_loose_mob_tick уже помечает весь коридор куска одним вызовом pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64) — с запасом на путь, пройденный за кадр. А потом три блита внутри mob_render помечают подмножества этого же прямоугольника, по 4 502 такта каждый: 3 × 4 502 × 6 кусков = 81 000 тактов впустую.

Механизм уже есть: pop_cd_batch_begin() / pop_cd_batch_end()pop_tile.c, применяется в draw_tile) — в пакете pop_cd_touch только копит общий прямоугольник четырьмя сравнениями. Обернуть mob_render в скобку.

Осторожно: пометка обязана покрывать весь коридор heal'а, а не габарит спрайта — иначе персонаж, попавший в расширенную часть коридора, стирается, но перерисовать себя не просит и исчезает с экрана (ровно этот баг ловили 2026-08-13: в комнате 23 после падения гряды пропадал Кид). Поскольку коридорную пометку из тика мы НЕ трогаем, инвариант сохраняется.

C2. pop_cd_touch — байтовые аргументы

Отдача: часть от 4 502 на вызов; после C1 в циане останется 6 вызовов (по одному на кусок), но в зелёной фазе и в fore-проходе их больше.

Четыре int-аргумента на вызов, 30 (ix) внутри. w/h заведомо ≤ 255 (габарит спрайта ≤ 56×63), x/y — экранные, 16-битные. Это позиция (а) задачи OPT-BLIT.

C3. blit_b_clip — 22 Б кадра, 211 (ix)

Отдача: 14 088 → ~10 500 на клипованный блит. В циане клипованный путь берут спрайты у краёв поля и весь fore-проход; в зелёной фазе — все блиты полосы у потолка. Подробнее — perf_green_phase.md §G3.

sx/sy/dw/dhuint8_t, dx/dy оставить 16-битными. Прецедент: тот же приём в pop_blit_b (коммит c312e4a) дал −19 % на клипованном пути.

C4. Разобрать pop_char_fore(KID): 1 722 → 150 978

Отдача неизвестна, но это 24 % фазы — мерить обязательно.

Гипотезы (проверять зондами, не рассуждением — memory defer_unexplained_quirks):

  1. пометки pop_cd_touch от кусков гасят pop_char_skip_mask для Кида, хотя куски падают в других колонках — проверить pop_cd_dmask в кадре;
  2. mob_mark_neighbour ставит pop_set_redraw_fore на 1–2 тайла на каждый кусок каждый кадр (порт draw_mob, seg007:1147), и pop_fore_needed перерисовывает передние части 6–12 тайлов;
  3. окно fore-клипа Кида расширено под клинок и брызги, и в него попадает больше тайлов, чем нужно — известный пункт 1 в perf_backlog.md («футпринт персонажа — брать из физики»): оригинал расширяет футпринт только на одну колонку при вынутом мече и объединяет с футпринтом прошлого кадра, а мы расширяем окном клипа.

Зондов на подфазы циана больше нет — понадобятся временные pop_dbg_* вокруг pop_fore_needed и pop_char_draw, то есть пересборка (после неё ВСЕ адреса зондов меняются, брать заново из roomtest.map).

C5. Один gfx_w0_map/unmap на группу блитов

Отдача: 1 086 + 264 = 1 350 на вызов; три части одного куска лежат в одной странице атласа → −2 700 на кусок, −16 000 на кадр.

Позиция 3 старого perf_backlog.md. Нужна форма «открыть страницу, N блитов, закрыть»; мешает то, что pop_blit_b — общий лист для всех вызывающих. Для mob_render (три блита из одного атласа pop_env) частный случай тривиален.

C6. Размеры ленты — из каталога атласа

Отдача: часть от 1 086 на вызов. Позиция 2 старого perf_backlog.md: fw/fh уже лежат в записи каталога (8 байт: offset u16, fw u8, fh u8, nx u8, ny u8, резерв u16), и у всех фоновых лент nx = ny = 1, то есть они в точности равны w/h из шапки. atlas_image их читает и выбрасывает.

C7. objtable вместо отдельного fore-прохода на персонажа

Позиции 5 и 6 старого perf_backlog.md — большой рефакторинг, браться только если после C1–C5 бюджет всё ещё не выполняется. Оригинал кладёт персонажей и куски в objtable и рисует их при обходе тайлов (draw_objtable_items_at_tile), а порядок окклюзии получается сам; у нас отдельный fore-проход НА КАЖДОГО персонажа.


3. Что НЕ делать

  • Не ставить W3-скобку из кода с --w3 — белый экран (memory gfx_blit_noclip_fast).
  • Не ускорять передачу пикселей — предел железа (memory blit_cost_model).
  • Не возвращать клип куска по clip.right = 40 оригинала (add_mob_to_objtable, seg007:1161): единицы этого поля не выяснены, буквальные 40 экранных пикселей срезают правый задний угол плиты (прогон 2026-08-13).

4. Журнал правок

дата что сделано циан: покой / Кид / пик коммит
2026-08-17 базовый замер (до правок этой серии) 20 550 / 193 000 / 632 000 c312e4a