Контрольный замер перед оптимизацией (задача 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>
13 KiB
ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу журнал правок — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
perf_l13_room23.md; там же ответ про 8-битные
габариты. Парная фаза — perf_cyan_phase.md.
Границы фазы в roomtest.c: от PROF(4) (строка 450) до PROF(6)
(строка 620). Содержимое: pop_loose_tick, pop_process_trobs,
pop_redraw_needed, шов, смена уровня, сигналы провалов, вспышка.
Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).
| состояние | зелёная |
|---|---|
| покой в комнате 23 | 35 760 |
| дрожат 6 плит-потолков | 366 000 |
| пик каскада (провалы + посадки) | 805 000 (2,0× бюджета) |
1. Раскладка (замер 2026-08-17, c312e4a, зонды m5/m6/m7 + kind/m16)
1.1 Верхний уровень
| участок | покой | дрожь | пик |
|---|---|---|---|
pop_loose_tick |
25 602 | 35 646 | 196 152 |
pop_process_trobs |
1 050 | 1 050 | 1 050 |
pop_redraw_needed |
924 | 319 944 | 737 322 |
| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 11 964 |
pop_redraw_needed — 91 % фазы на пике.
1.2 pop_loose_tick изнутри (зонды m9..m12)
| участок | покой | пик |
|---|---|---|
| два цикла по тайлам (30 + 10 позиций) | 11 004 | 9 852 |
pop_loose_mob_tick (heal летящих кусков) |
7 416 | 178 494 |
check_loose_fall_on_kid |
6 120 | 3 480 |
| хвост | 324 | 3 252 |
pop_loose_mob_tick при шести кусках в воздухе — ≈29 700 такта на кусок.
Это heal прошлой позиции (коридор MOB_W × 64) плюс pop_cd_touch на
коридор плюс mob_tick_one.
1.3 Цена ОДНОЙ перерисовки по видам (66 + 12 + 12 замеров)
| вид | штук замерено | средняя цена | сколько за кадр | Σ за кадр |
|---|---|---|---|---|
RDA_CEIL — дрожащая плита-потолок |
66 | 48 658 | до 6 | 292 000 |
RDA_CEIL_GONE — запечь колодец |
12 | 251 023 | до 2 | 619 000 |
RD_FLOOR — щебень на месте посадки |
12 | 198 259 | до 2 | 397 000 |
Для масштаба: ОДНА запечка колодца = 58 % растрового кадра, две = 144 %.
pop_dbg_rdmax подтверждает состав пика: шесть пометок, все RDA_CEIL.
1.4 RDA_CEIL = 48 658 изнутри (зонды m13/m14/m15)
| участок | такты |
|---|---|
pop_heal_off(x, 0, 64, CEIL_BAND_H=8) |
8 491 |
draw_tile(-1, col) |
39 029 |
Блитов внутри этого draw_tile — 1,17 на тайл (7 вызовов pop_blit_b
на 6 тайлов), то есть 15–16 тыс. тактов.
Остальные ~23 700 такта — накладные draw_tile, ни одного пикселя.
1.5 RDA_CEIL_GONE = 251 023 и RD_FLOOR = 198 259
pop_ceil_bake_empty(col)— бар + шестьdraw_tile(ряд −1 и ряд 0, колонки col−1..col+1): 6 × 39 000 ≈ 234 000.pop_floor_bake(row,col)— бар 60×39 отyb+26+ дваdraw_tileНИЖНЕГО ряда; там стена, то естьwall_pattern— ≈99 000 на тайл.
1.6 Почему draw_tile стоит 39 000 при одном блите
Кодоген. Подсчёт в bank7_pop_room.asm и совпадение с замером до процентов
(одно -N(ix) ≈ 19 номинальных тактов ≈ 46 замеренных):
| функция | стековый кадр | обращений (ix) |
ожидаемо | замерено |
|---|---|---|---|---|
draw_tile |
26 Б | 513 | ~23 600 | ~23 700 |
pop_ceil_bake_empty |
— | 8 | ~370 | |
pop_floor_bake |
— | 12 | ~550 | |
mob_tick_one |
16 Б | 99 | ~4 550 | из ~29 700/кусок |
mob_draw_pass |
18 Б | 20 | ~900 |
У draw_tile пятнадцать локалей, объявленных int
(code/lcode/lmod/xh/x/dby/dmy/t/lt/rbl_code/rbl_mod/
wmod/base_id/base_yb), — в регистры они не влезли, и функция целиком
уехала в стековый кадр. Байтовые по природе из них все, кроме x/dby/dmy
(экранные координаты, бывают отрицательными).
2. Способы ускорения
По убыванию отдачи. Оценки — от замеров выше; каждая позиция сверена с
SDLPoP/src/seg008.c (правило проекта: механику сверять с оригиналом ДО
кодинга).
G1. Окно клипа для ЗАПЕЧКИ — самый крупный кусок
Отдача: −250 000 … −400 000 на пиковый кадр.
pop_ceil_bake_empty и pop_floor_bake восстанавливают ИЗВЕСТНЫЙ
прямоугольник (тот, который сами же залили баром), а зовут полный
draw_tile, который честно рисует и то, что в этот прямоугольник не попадает.
У pop_floor_bake бар — 60×39 от yb+26, а тело дворцовой кладки рисуется
НИЖЕ yb+64, то есть вся стена рисуется мимо цели.
В pop_blit_b уже есть дешёвый предфильтр по окну pop_t_fclip_* — он режет
кусок ДО atlas_image и gfx_w0_map, то есть почти даром. Достаточно
выставлять это окно на время запечки.
Грабли, которые надо обойти: pop_t_fclip_on в pop_blit_b заодно подавляет
pop_cd_touch (признак «это fore-проход поверх персонажа»). Для запечки это
как раз правильно — она помечает область сама, — но проверить обязательно;
если понадобится, ввести отдельный флаг «окно только для отсева».
Сверка с оригиналом: у него та же идея, только через wipetable —
draw_tile_wipe(height) кладёт прямоугольник в таблицу, а draw_table
рисует уже с известным клипом (seg008:1368, 1373).
G2. Расколоть draw_tile + отдельный лист для ряда −1
Отдача: 39 029 → 18 000–20 000 на тайл полосы, то есть −140 000 на кадр дрожания; и она умножается на G1 (шесть вызовов в запечке).
Два шага:
- Отдельный
draw_tile_aboveroom(col)— дословный порт seg008:01F2. Оригинал для ряда −1 делает РОВНО шесть вещей:draw_tile_floorright,draw_tile_anim_topright,draw_tile_right,draw_tile_bottom(1),draw_loose(1),draw_tile_fore. Ни базы, ниdraw_tile_anim, ниdraw_tile_anim_right, ниwall_pattern, ни вычисленияrbl_*черезrow+1 > 2. У нас всё это отсекается веткамиif (row >= 0)уже ВНУТРИ общегоdraw_tile— то есть кадр IX, пролог и вся арифметика оплачиваются всегда. - Расколоть общий
draw_tileпо образцу оригинала (seg008:01C7 — девять узких листьев). Каждый лист получаетxh/dby/dmyпараметрами (в регистрах), локалей у него единицы, кадра IX нет.
Заодно проверить redraw_needed_above (seg008:02C1): оригинал делает
draw_tile_wipe(3) — стирает 32×3, а мы pop_heal_off(x,0,64,8) =
64×8. Площадь в 5 раз больше нужной (8 491 такта), хотя основная цена там —
накладные вызова.
G3. blit_b_clip — 22 Б кадра, 211 (ix)
Отдача: 14 088 → ~10 500 на блит. В зелёной фазе ВСЕ блиты клипованные
(полоса у потолка выставляет pop_t_clip_top) — при 7–17 блитах это
−25 000 … −60 000; в циане и fore-проходе больше.
dx/dy/dw/dh/sx/sy объявлены int. По построению sx/sy/dw/dh ≤ 255
(габарит любого спрайта ≤ 56×63, см. perf_l13_room23.md §5) — их можно
сделать uint8_t, оставив 16-битными только экранные dx/dy.
Прецедент: тот же приём в pop_blit_b (коммит c312e4a) дал −19 % на
клипованном пути и −16 % на быстром, _CODE −42 Б.
Проверка по правилу OPT-BLIT: после правки смотреть пролог в
.sprinter-cc-roomtest/pop_tile.asm — исчез ли ld iy,#-22, сколько
осталось -N(ix).
G4. mob_tick_one / pop_loose_mob_tick — 29 700 на кусок
Отдача: −30 000 … −48 000 на кадр при шести кусках.
16-байтовый кадр, 99 (ix). Байтовыми могут быть габарит heal-коридора и
разность prev_y; x/y — 16-битные. Плюс pop_cd_touch на коридор — один
на кусок, 4 502 такта (см. perf_cyan_phase.md §1.3 про его цену).
G5. LOOSE-SHAKE-RUNS — пометки по сменам кадра
Отдача: ~20 % работы фазы, и только на дрожащих плитах. Оценка старая (2026-08-13), не перемерена.
Подробности и арифметика — в roomtest/TASKS_OPEN.md#loose-shake-runs.
Коротко: наивный вариант «помечать только на смене кадра» выигрыша НЕ даёт
(пять смен × две страницы = те же десять перерисовок); работает только версия
«пары и тройки» — две пометки с pages = 2 на фазах 5 и 8. Тот же приём
применим к плитам-ПОТОЛКАМ (pop_set_redraw_above), где сейчас идёт пометка
каждый кадр отсчёта.
Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться после G1–G4.
G6. BG-ONCE — heal вместо чёрного бара
Предложение пользователя, см. roomtest/TASKS_OPEN.md#bg-once. Сейчас
запечка = «залить чёрным + перерисовать фон», то есть двойная работа; heal по
координатам бара делает то же за один проход. Прямо дополняет G1.
3. Что НЕ делать (проверено, отрицательный результат)
- Не ускорять передачу пикселей — она на пределе железа (3+3 такта на
байт через акселератор), memory
blit_cost_model. В пиковом кадре «железный» минимум всех блитов ≈150 000 из 1 437 150. - Не откладывать запекание на другой кадр — запечка пишет ОЗУ-копию, из которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры (проверено 2026-08-13).
- Не батчить смежные колонки в
pop_ceil_shake_draw— пробовалось и убрано: плиты стартуют со случайными задержками, в кадре дрожат разрозненные колонки, пробег почти всегда длиной в одну. - Не искать проблему в W3-скобке (
_bgi_begin/_bgi_end— пять инструкций) и не списывать разброс на прерывания (внутри размерной группы разброс три такта).
4. Журнал правок
| дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|---|---|---|---|
| 2026-08-17 | базовый замер (до правок этой серии) | 35 760 / 366 000 / 805 000 | c312e4a |