Files
Sprinter-SDCC/applications/PoP/docs/perf_green_phase.md
T
snark13 f44663040c Регресс тактов на 23/13 после обхода уровней 1-2: изменений нет
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).

                работа     синяя   зелёная      циан
af189a1        916 458   142 830   546 900   270 510
40f0d46        873 930   158 874   417 630   379 482
ec1f384        878 550   158 880   419 526   379 488

Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона.  Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.

Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000.  Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:49:02 +03:00

23 KiB
Raw Blame History

ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация

Рабочий документ: живёт между сессиями. Внизу журнал правок — каждая запись с замером до/после. Сцена, рецепт воспроизведения и зонды — 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).

состояние было (c312e4a) стало (af189a1)
покой в комнате 23 35 760 35 760
дрожат 6 плит-потолков 366 000 335 400
пик каскада 805 000 546 900

ЦЕЛЬ ФАЗЫ НЕ ДОСТИГНУТА: 546 900 против 400 000 (1,37×). Что осталось сделать и почему это именно раскол draw_tile — §3.


1. Раскладка ПОСЛЕ правок (замер af189a1)

Пик — кадры 24-28 (посадки плит), больше не кадры провалов.

участок покой дрожь пик
pop_loose_tick 36 234 36 234 156 762
pop_process_trobs 1 050 1 050 1 050
pop_redraw_needed 924 288 800 380 568
хвост (шов, смена уровня, сигналы, вспышка) 9 336 9 336 10 776

pop_loose_tick изнутри на пике: два цикла по тайлам 9 852, pop_loose_mob_tick 154 074 (heal шести летящих кусков), остальное мелочь.

Цена одной перерисовки: было → стало

вид было стало чем
RDA_CEIL — дрожащая плита-потолок 48 785 43 536 G2 + G3
RDA_CEIL_GONE — запечь колодец 251 335 138 318 G1 (клип полосы) + G2 + G3
RD_FLOOR — щебень на месте посадки 198 805 179 914 G2 + G3 + снятая двойная пометка

Штук за кадр: RDA_CEIL до 6, RDA_CEIL_GONE до 2, RD_FLOOR до 2.

Детальный профиль RD_FLOOR (179 914) — главная оставшаяся статья

Снят зондами по каждому блиту (pop_dbg_b1/b5) и по рамкам (pop_bar_black, pop_cd_batch_begin/end):

участок такты
вход pop_floor_bake + gfx_set_bank 2 382
pop_bar_black 60×39 (включая pop_cd_touch 4 502) ~15 500
контекст draw_tile #1 (5 чтений тайлов, 63*row, индексация таблицы) 13 584
4 блита тайла #1 50 718
диспетчер между блитами #1 (все if (code == …)) 11 388
контекст draw_tile #2 13 584
4 блита тайла #2 ~54 000
диспетчер между блитами #2 11 388
хвост 6 474

Итого: 105 500 — сами блиты (реальные пиксели), 74 400 — накладные, из которых 27 168 контекст двух draw_tile и 22 776 их диспетчер.


2. Что сделано (с чем сравнивать)

G1. Окно клипа для точечной перерисовки — −90 000

pop_t_win_set(x, ytop, w, h) / pop_t_win_clear() в pop_tile.c: ставит уже существующее окно pop_t_fclip_* на прямоугольник, который перерисовка восстанавливает. Работает в обе стороны — и предфильтр pop_blit_b отсеивает куски мимо окна ДО atlas_image/gfx_w0_map, и blit_b_clip режет остальные по нему.

Где сработало: pop_ceil_bake_empty — куски ряда 0 высотой 63 px рисовались целиком, хотя восстановить надо девять строк полосы. Блит 21 447 → 7 619, вся перерисовка 251 335 → 138 318.

Где НЕ сработало — см. §4, отрицательные результаты.

Побочно: пока окно стоит, pop_blit_b не ставит пометку «фон трогали» (признак fore-прохода), поэтому вызывающий обязан пометить прямоугольник сам. В pop_ceil_shake_draw добавлен явный pop_cd_touch на область heal'а; в pop_ceil_bake_empty и pop_floor_bake метит pop_bar_black, а лишний второй вызов на ту же область снят.

G2. Контекст тайла — file-scope, а не локали draw_tile55 000

Порт load_curr_and_left_tile (seg008:0339): у оригинала это curr_tile/curr_modifier/draw_xh/draw_main_y/draw_bottom_y — переменные модуля, а не локали.

Причина в кодогене: в draw_tile 57 вызовов, и каждое живое через вызов значение SDCC спиливал в стековый кадр — 26 байт кадра и 513 обращений -N(ix) (при ~46 замеренных тактах на обращение это ~23 600, что и намерено). Стало 33 обращения, кадра нет, банк 7 −703 Б.

G3. blit_b_clip — байтовый габарит + file-scope

Два шага, и важен порядок наблюдений:

  1. Байтового габарита ОДНОГО НЕ ХВАТИЛО. sx/sy/dw/dhuint8_t (корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а 22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi, всё равно больше, чем регистров у Z80.
  2. Решило вынесение из локалей (bc_*): 51 обращение, кадр 22 → 12 Б.

Клипованный блит 14 088 → 11 848. Заодно blit_b_oversize больше не ходит через blit_b_clip (там теперь байтовый габарит) — рисует напрямую gfx_blit_part; это путь под полноэкранные подложки интро/финала.

G4. Мелочи

  • pop_blit_b: аргументы в file-scope (третий и дальше SDCC передаёт стеком, каждое чтение шло через -N(ix)) — 76 → 11 обращений.
  • pop_loose_mob_tick: пометки всех кусков ОДНИМ пакетом (pop_cd_batch_begin/end) — было по 4 502 такта на кусок. 176 772 → 168 600.
  • Коридор heal куска — по фактической высоте СОБРАННОГО композита (было 24 строки константой, стало 20). 168 600 → 156 762.

3. Что осталось: раскол draw_tile (позиция G5)

Оставшийся разрыв: −147 000. Он весь в двух местах.

G5. Расколоть draw_tile на узкие части, как в оригинале

Ожидание: 50 000 … 60 000.

У оригинала draw_tile (seg008:01C7) — это девять независимых вызовов: draw_tile_floorright, draw_tile_anim_topright, draw_tile_right, draw_tile_anim_right, draw_tile_bottom, draw_loose, draw_tile_base, draw_tile_anim, draw_tile_fore. Для ряда −1 он зовёт шесть из них (draw_tile_aboveroom, seg008:01F2), для полосы у потолка — те же шесть плюс draw_tile_wipe(3) (redraw_needed_above, seg008:02C1).

У нас всё это — ветки if (row >= 0) ВНУТРИ одной функции, то есть контекст (13 584) и диспетчер (11 388) оплачиваются целиком всегда. Расколов, каждая точечная перерисовка сможет звать только нужные части:

  • pop_floor_bake: вместо второго полного draw_tile(row, col+1) — только его правую грань и базу;
  • pop_ceil_shake_draw / pop_ceil_bake_empty: дословный draw_tile_aboveroom;
  • pop_loose_shake_draw, pop_spike_redraw, pop_gate_redraw — то же.

Риск средний: у draw_tile собрано много инвариантов (BUG-LOOSE-3, BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1), проверять придётся прогонами всех уровней. Поэтому делать отдельным заходом, а не хвостом другой правки.

G6. Меньше блитов в RD_FLOOR

Ожидание: неизвестно, надо мерить. 105 500 из 179 914 — это 7,6 блита, и они рисуют настоящие пиксели. Сократить можно только сократив то, что восстанавливается: бар сейчас 60×39 от yb+26, а плита занимает по вертикали меньше (её куски: левая грань POP_LOOSE_FRAM_LEFT 32×13 на dmy = yb+62, низ POP_LOOSE_FRAM_BOTTOM 32×3 на dby = yb+65, правая грань в соседе 26×16 на dby1). То есть плита живёт в yb+47 .. yb+65, а бар начинается с yb+2621 лишняя строка сверху.

Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла (орнаментная лента stripe_id на dmy27 = yb+35 попадает как раз в «лишнюю» часть). Сузишь бар — надо убедиться, что ничего не осталось.

G7. heal летящих кусков — 154 074 (28 % фазы)

Шесть кусков × ~25 700: сам heal 64×20 (по модели ~19 700) + пакетная пометка + накладные mob_tick_one (16-байтовый кадр, 99 обращений (ix)). Сам heal у предела железа — это 1 280 пикселей на кусок, оптимизировать нечего, кроме площади. Площадь уже подрезана до габарита композита.

Остаётся mob_tick_one (~4 500 на кусок = 27 000 на кадр) — то же лечение file-scope, что у draw_tile.

G8. Пометка соседа — узкой полосой, а не полным тайлом (идея пользователя)

Ожидание: заметное, но не мерено. Взять ПОСЛЕ обхода всех уровней (решение пользователя 2026-08-17: пока идёт отлов багов слоёв, каждая правка добавляет переменных в картину).

Когда плита (1,8) падает, помечаются ДВА тайла:

пометка что делает
(1,8)RD_LOOSE_GONE бар 40 на своём x, бар 32 на соседе, draw_tile(1,8) + draw_tile(1,9)
(1,9)RD_FLOOR бар 60 на x соседа, draw_tile(1,9) ЕЩЁ РАЗ

То есть сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО, хотя потревожили у него только левые 28 пикселей — там, куда свисает правая грань упавшего тайла. draw_tile(1,9) при этом вызывается дважды на одну пометку.

Что такое эти числа (чтобы не сузить лишнего):

  • 60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес. Правая грань пола (кадр 42, 26 px) рисуется в клетке соседа с x+32, занимая x+32..x+57. Для запечки САМОГО тайла 60 уже минимальны — сужать их нельзя;
  • сузить можно только тот случай, когда тайл помечен ПОТОМУ ЧТО ИЗМЕНИЛСЯ ЕГО ЛЕВЫЙ СОСЕД: тогда нужна полоса 28 px у левого края, а не весь тайл.

Почему выигрыш не символический: pop_floor_bake стоит 179 914 тактов, из них 105 500 — сами блиты. Узкая полоса срезала бы и площадь бара (60×39 → 28×39), и часть блитов — окно клипа там теперь стоит обязательным (см. §4), так что отсев достаётся даром.

Условия, из-за которых это не «просто уменьшить число»:

  1. pop_floor_bake — ОБЩАЯ функция: её же зовут кнопка (pop_button_redraw), зеркало, подобранный предмет и щебень на месте посадки. Там меняется сам тайл и 60 нужны целиком. Значит нужен отдельный вход (напр. pop_floor_bake_edge(row, col)) или параметр-прямоугольник — именно под пометку «изменился мой левый сосед».
  2. Прежде чем выкидывать вторую пометку целиком, сверить ВЕРТИКАЛЬНЫЕ диапазоны: pop_loose_bake_empty кроет 63*row+46 .. +65 (20 строк), а pop_floor_bakeyb+26 .. yb+64 (39 строк). То есть сосед покрыт ВТОРЫМ баром не полностью, и просто снять пометку нельзя.
  3. Ширина полосы = свес ЛЕВОГО тайла, а он зависит от типа тайла (у loose это 8 px по комментарию в pop_loose_bake_empty, у пола 26). Брать по максимуму (28) — безопасно.

G9. Снять временную оснастку

Шесть pop_dbg_b1..b6 внутри pop_blit_b — ~400 такта на блит; при 15 блитах зелёной это 6 000 на кадр. Плюс pop_dbg_kind/m16 (2 вызова на перерисовку) и pop_dbg_m5..m15. Снимать ПОСЛЕ окончания оптимизации: без них не мерить.


4. Копия второй страницы — и почему она ТРЕБУЕТ окна клипа

Точечные запечки ставятся с pages = 2, срабатывают два кадра подряд (по разу на страницу дабл-буфера) и оба раза считают одно и то же. После первого раза нужный прямоугольник уже лежит в ОЗУ-копии первой страницы, и его можно скопировать: gfx_copy_page берёт источником ОЗУ-копию НЕактивной страницы (то есть ЧИСТЫЙ фон — спрайты рисуются банком без тени и в копию не попадают), а приёмник обновляет и в видео-ОЗУ, и в ОЗУ-копии. Идея пользователя: тот же приём, что при перевороте экрана (зелёное зелье), только без зеркала.

полная запечка копия
щебень / кнопка (60×39) 179 914 ~35 000
колодец полосы потолка (64×9) 138 318 ~17 500

ДВА УСЛОВИЯ КОРРЕКТНОСТИ. Оба нарушались и оба дали видимые баги.

  1. Запечка обязана быть ОГРАНИЧЕНА копируемым прямоугольником. draw_tile рисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара; копия переносит ровно бар, и всё, что легло вне него, на второй странице остаётся прежним — страницы расходятся, это видно как МЕРЦАНИЕ через кадр. У полосы потолка окно стояло с самого начала (G1), у pop_floor_bake — нет, и он мерцал торцами полов, плит и кнопок (найдено пользователем 2026-08-17: уровень 1, комната 6, Кид на кнопке (0,2)). Поэтому в pop_floor_bake окно теперь стоит КАК УСЛОВИЕ КОРРЕКТНОСТИ, хотя по скорости само по себе убыточно (см. §5) — снимать его нельзя.
  2. Копия годится только если содержимое тайла между двумя кадрами не изменилось. У анимированного тайла (кнопка с идущим таймером связи) пометка обновляется КАЖДЫЙ кадр и картинка каждый раз другая. Поэтому pop_set_redraw/pop_set_redraw_above гасят слот копии при ПЕРЕпометке (pop_bake_slot_reset*).

Плюс слот bake_pg/bake_pg_above помнит, НА КАКОЙ странице сделана первая запечка: копируем только если первая была на ДРУГОЙ странице и дабл-буфер включён. Это покрывает переплетение двух запечек в одном кадре, однобуфер (чит SPACE) и смену комнаты (pop_bake_forget).


5. Отрицательные результаты — НЕ повторять

Окно клипа в pop_floor_bake — по СКОРОСТИ проверено ТРИ раза, каждый раз хуже

Но оно всё равно стоит на месте: без него ломается копия второй страницы (§4). Ниже — только про скорость самого окна.

попытка было стало
до G3 187 266 198 279
после G3 182 124 188 460
после pop_blit_b file-scope, с детальным зондом 179 914 188 417

Третья попытка объяснила причину: клипованный путь стоит +1 500 такта на КАЖДОМ из 7,6 блитов (+11 400), а режет он только редкие высокие куски — в трассе такие нашлись (34 878 → 25 872 и 28 818 → 24 090, всего 13 700), но в среднем по 12 перерисовкам их нет. Запись стоит в коде.

Прочее (проверено раньше)

  • Не откладывать запекание на другой кадр — запечка пишет ОЗУ-копию, из которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры.
  • Не батчить смежные колонки в pop_ceil_shake_draw — плиты стартуют со случайными задержками, в кадре дрожат разрозненные колонки, пробег почти всегда длиной в одну.
  • Не ускорять передачу пикселей — предел железа (3+3 такта на байт, memory blit_cost_model). В пиковом кадре «железный» минимум всех блитов ≈150 000 из 916 458.
  • LOOSE-SHAKE-RUNS (пометки по сменам кадра): наивный вариант выигрыша НЕ даёт — пять смен × две страницы = те же десять перерисовок. Работает только версия «пары и тройки», ~20 % и только на дрожащих плитах; оценка 2026-08-13, не перемерена. Подробности — roomtest/TASKS_OPEN.md#loose-shake-runs.

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

дата что сделано зелёная: покой / дрожь / пик коммит
2026-08-17 базовый замер 35 760 / 366 000 / 805 000 c312e4a
2026-08-17 G2 контекст тайла в file-scope — / — / 747 954 a3c473d
2026-08-17 G1 окно клипа в pop_ceil_bake_empty — / — / 663 250 a3c473d
2026-08-17 G3 blit_b_clip байты + file-scope — / 335 400 / 575 730 a3c473d
2026-08-17 pop_blit_b file-scope; пакетная пометка кусков — / — / 557 706 b2da0b8
2026-08-17 коридор heal по высоте композита 35 760 / 335 400 / 546 900 9a50ab2
2026-08-17 копия второй страницы вместо второй запечки — / — / 423 558 5ef721e
2026-08-17 mob_tick_one в file-scope; снята оснастка из горячих путей 35 760 / 326 130 / 414 456 18ee60e
2026-08-17 фикс мерцания: окно клипа в pop_floor_bake как условие корректности копии замер после фикса — ниже 35b7cd5
2026-08-17 замер после фиксов уровня 1 (мерцание торцов, потолочный fore, сосед под плитой, блеск меча) 35 760 / — / 417 630 40f0d46
2026-08-17 регресс после фиксов уровня 2 (чёрные бары, чит бессмертия) — в пределах шума — / — / 419 526 ec1f384