Замер 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>
23 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).
| состояние | было (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_tile — −55 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
Два шага, и важен порядок наблюдений:
- Байтового габарита ОДНОГО НЕ ХВАТИЛО.
sx/sy/dw/dh→uint8_t(корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а 22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi, всё равно больше, чем регистров у Z80. - Решило вынесение из локалей (
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 на dby−1). То есть плита живёт в yb+47 .. yb+65, а бар начинается с
yb+26 — 21 лишняя строка сверху.
Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла
(орнаментная лента stripe_id на dmy−27 = 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), так что отсев достаётся даром.
Условия, из-за которых это не «просто уменьшить число»:
pop_floor_bake— ОБЩАЯ функция: её же зовут кнопка (pop_button_redraw), зеркало, подобранный предмет и щебень на месте посадки. Там меняется сам тайл и 60 нужны целиком. Значит нужен отдельный вход (напр.pop_floor_bake_edge(row, col)) или параметр-прямоугольник — именно под пометку «изменился мой левый сосед».- Прежде чем выкидывать вторую пометку целиком, сверить ВЕРТИКАЛЬНЫЕ
диапазоны:
pop_loose_bake_emptyкроет63*row+46 .. +65(20 строк), аpop_floor_bake—yb+26 .. yb+64(39 строк). То есть сосед покрыт ВТОРЫМ баром не полностью, и просто снять пометку нельзя. - Ширина полосы = свес ЛЕВОГО тайла, а он зависит от типа тайла (у 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 |
ДВА УСЛОВИЯ КОРРЕКТНОСТИ. Оба нарушались и оба дали видимые баги.
- Запечка обязана быть ОГРАНИЧЕНА копируемым прямоугольником.
draw_tileрисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара; копия переносит ровно бар, и всё, что легло вне него, на второй странице остаётся прежним — страницы расходятся, это видно как МЕРЦАНИЕ через кадр. У полосы потолка окно стояло с самого начала (G1), уpop_floor_bake— нет, и он мерцал торцами полов, плит и кнопок (найдено пользователем 2026-08-17: уровень 1, комната 6, Кид на кнопке (0,2)). Поэтому вpop_floor_bakeокно теперь стоит КАК УСЛОВИЕ КОРРЕКТНОСТИ, хотя по скорости само по себе убыточно (см. §5) — снимать его нельзя. - Копия годится только если содержимое тайла между двумя кадрами не
изменилось. У анимированного тайла (кнопка с идущим таймером связи)
пометка обновляется КАЖДЫЙ кадр и картинка каждый раз другая. Поэтому
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 |