Контрольный замер перед оптимизацией (задача 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
ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу журнал правок — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
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/dh → uint8_t, dx/dy оставить 16-битными. Прецедент: тот же
приём в pop_blit_b (коммит c312e4a) дал −19 % на клипованном пути.
C4. Разобрать pop_char_fore(KID): 1 722 → 150 978
Отдача неизвестна, но это 24 % фазы — мерить обязательно.
Гипотезы (проверять зондами, не рассуждением — memory
defer_unexplained_quirks):
- пометки
pop_cd_touchот кусков гасятpop_char_skip_maskдля Кида, хотя куски падают в других колонках — проверитьpop_cd_dmaskв кадре; mob_mark_neighbourставитpop_set_redraw_foreна 1–2 тайла на каждый кусок каждый кадр (портdraw_mob, seg007:1147), иpop_fore_neededперерисовывает передние части 6–12 тайлов;- окно 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— белый экран (memorygfx_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 |