Files
Sprinter-SDCC/applications/PoP/docs/perf_registry.md
T
snark13 f81b30eb68 Замер P2 и P6: главные статьи — физика двух Char и луч видимости
Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs
(89 784) — на три.

Гипотеза, с которой я входил в замер, ОТВЕРГНУТА.  Я ждал, что дорого
обходятся банковые трамплины на спецсобытиях уровней — по аналогии с
pop_clip_char_top, где трамплин ради одной проверки стоит 8 892.  На деле
три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они
гейтятся внутри и на уровне 11 выходят сразу.

Настоящие статьи синей:

  физика двух Char        105 246  (61 266 Кид + 43 980 страж)
  heal двух Char           67 734
  луч видимости стража  до 37 032  (вместе с frame_timers)
  pop_ctrl_tick            18 648
  логика стража            19 932

Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр
позы не меняется.  Цена измерена, причина нет — это отдельная позиция P2a.
Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене
позиции/комнаты — позиция P2b, ждём −30 000.  heal отдельной правки не
требует, он уйдёт вместе с P1/P3.

process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход
самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ
trob, хотя комната одна), два факела ~36 000.  Цена одного pop_pot_b
измерена отдельно: 17 346, и это единственная группа в распределении —
значит в кадре его зовут только факелы.  Пиксели пламени 16x18 — 1 716,
то есть 10 % цены.

Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма
ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной
(P1).  Цель достижима, но без запаса.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:05:58 +03:00

25 KiB
Raw Blame History

Реестр оптимизаций: всё отложенное, в одном списке

Собрано 2026-08-19 из perf_green_phase.md (G1-G9), perf_cyan_phase.md (C1-C7), perf_backlog.md (позиции 1-7), ../roomtest/TASKS_OPEN.md (HEAL-WIDTH) и из свежего разбора сцены perf_l11_room15.md.

База для процентов — работа кадра в 11/15: 801 768 тактов (замер 09f32ce). Где эффект относится к другой сцене, это сказано явно.

Оценки помечены: [замер] — измерено; [модель] — посчитано по измеренным составляющим; [гипотеза] — не мерено, нужен прогон.


1. Цена одного блита фона — разложена [замер 2026-08-19]

Метод: брейкпоинты на резидентных адресах внутри pop_blit_b (0x5C50), temp0 на входе, разница totalcycles на каждом вызове. 1603 блита.

этап такты постоянство
пролог + аргументы + грубый отсев 810 ровно, всегда
atlas_image 672 ровно, всегда
gfx_w0_map + чтение шапки ленты + арифметика клипа 2 400 ровно, всегда
ядро блита (пиксели) 4 380 … 26 592 по размеру кадра
pop_cd_touch 2 069 (пакетный путь) / 4 115 (настоящая пометка) почти ровно
gfx_w0_unmap + эпилог 175 ровно, всегда
весь блит 10 458 … 32 718, медиана 16 674

Фиксированная накладная = 6 126 тактов на любой блит, хоть 8×8. У самого дешёвого блита (10 458) это 59 % цены; у пламени факела 16×18 пиксели тянут ~1 700 из ~14 000, то есть 12 %.

Это и есть ответ на вопрос «почему маленький блит стоит 14 000». Причины ровно те, о которых спрашивал пользователь:

  • 810 на прологcall ___sdcc_enter_ix, IX-фрейм и шесть чтений N(ix): третий и четвёртый аргументы (int x, int ybottom) идут СТЕКОМ, каждое обращение 19 тактов Z80;
  • 2 069 на pop_cd_touch даже по пакетному пути, где вся работа — четыре сравнения. Сигнатура (int x, int y, int w, int h) = 8 байт аргументов, два из них через стек; w/h никогда не больше 64, y не больше 255, то есть три из четырёх могли быть uint8_t;
  • 2 400 на map + шапкуgfx_w0_map (1 086) + unmap (264, платится в конце) + ~1 000 на чтение четырёх байт заголовка и арифметику;
  • 672 на atlas_image — маппинг W3, чтение записи каталога, возврат W3; размеры при этом читаются и выбрасываются (см. §3, C6).

Блитов за кадр в 11/15: 8 [замер] — все в зелёной фазе (6 на draw_tile чомпера, 2 на пламя факелов). Синяя и циан через pop_blit_b не ходят вовсе: heal и персонажи идут своими путями. Значит фиксированные накладные блита стоят сцене 8 × 6 126 = 49 000 тактов/кадр (6,1 % работы).


2. Модель зелёной фазы 11/15 [модель, сходится с 320 916 замера]

статья такты доля фазы
8 блитов фона (из них 6 126×8 = 49 000 накладных) 133 400 41 %
диспетчер draw_tile (один тайл чомпера) 56 200 18 %
цикл pop_process_trobs без блитов пламени 59 100 18 %
pop_loose_tick (плит в комнате НЕТ) 28 872 9 %
heal 32×64 в pop_chomp_redraw 34 000 11 %
шов / ворота соседа 9 336 3 %

Больше половины фазы — не пиксели, а обвязка вокруг них.


3. Список приёмов, отсортированный по эффекту

P1. Чомпер под факелом — возвращать только слой anim [модель: −160 000, 20 % работы]

Разбор в perf_l11_room15.md §3. Сейчас пометка от факела обрабатывается как heal 32×64 + полный draw_tile (190 260 тактов на единственный перерисованный тайл кадра); оригинал в этом случае рисует ТОЛЬКО draw_tile_anim — графику чомпера поверх свежего пламени.

Останется 1-2 блита челюстей ≈ 20 000-33 000. Риск низкий: это сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку «фон трогали» вокруг тайла чомпера — см. P3.

Новое, найдено 2026-08-19.

P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась

Замер зондами внутрь обеих половин синей (286 518 тактов):

участок такты доля работы
ввод + читы 14 154 1,8 %
три спецсобытия уровней (skel / mouse / killed_shadow) 1 650 0,2 %
pop_frame_timers + луч видимости стража 37 032 4,8 %
pop_ctrl_tick 18 648 2,4 %
skip_mask + heal двух Char 67 734 8,8 %
mirror_heal + fore_heal 2 040 0,3 %
kid_tick (play_seq) 10 098 1,3 %
pop_phys_tick (физика Кида) 61 266 8,0 %
pop_guard_tick (логика стража) 19 932 2,6 %
pop_guard_phys_tick (физика стража) 43 980 5,7 %
боёвка (sword_hurting / sword_hurt / delta_hp) 9 558 1,2 %
guard_fallout + уход из комнаты 366

Что оказалось не так, как ждали. Я предполагал, что дорогие тут банковые трамплины на спецсобытиях (по аналогии с pop_clip_char_top, 8 892 такта за трамплин ради одной проверки). Замер это отверг: три спецсобытия уровней вместе стоят 1 650 — они гейтятся внутри и на уровне 11 честно выходят сразу.

Настоящие статьи — три:

  1. Физика двух Char — 105 246 (61 266 + 43 980), при том что оба персонажа СТОЯТ и кадр позы не меняется. Это 13,7 % работы кадра и самая крупная статья синей. Нужен ещё один уровень разбора — внутрь pop_phys_tick (позиция P2a, отдельным заходом).
  2. Луч видимости стража — до 37 032 (вместе с pop_frame_timers, но тот заведомо копеечный: три счётчика). Считается КАЖДЫЙ кадр, хотя ни Кид, ни страж не сдвинулись. Кандидат на гейт «пересчитывать только при смене позиции или комнаты любого из двоих» — позиция P2b, ожидание −30 000, риск низкий.
  3. heal двух Char — 67 734. Отдельной правки не требует: он платится ровно потому, что skip_mask никого не пропускает, и уйдёт вместе с P1/P3.

Новое, найдено 2026-08-19.

P3. Персонажи будятся каждый кадр [модель: −70 000, следствие P1]

heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то, что pop_char_skip_mask не может пропустить ни Кида, ни стража: фон трогают каждый кадр.

  • Кида спасает P1: широкая пометка вокруг чомпера исчезнет;
  • стража спасти нельзя — пламя правого факела (0,7) рисуется в ячейке (0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже перерисовывает.

P4. Накладные блита — четыре независимые правки [модель: −28 000 в 11/15; больше в сценах с 20+ блитами]

правка на блит источник
pop_cd_touch: uint8_t вместо int для y/w/h, ранний выход пакетного пути ~1 300 новое
один gfx_w0_map/unmap на ГРУППУ блитов ~1 350 C5 / backlog §3
размеры ленты из каталога, без atlas_image и чтения шапки ~670 C6 / backlog §2
pop_blit_b: аргументы в 8 бит, где хватает ~400 новое

Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из 6 126 фиксированных.

P5. pop_loose_tick при пустой комнате — 28 872 → 2 760 СДЕЛАНО 2026-08-19

Получено −26 112 внутри функции, −33 840 на кадре (замер до/после в 11/15). Оценка была 28 000.

Раскладка холостого хода (замер зондами m9..m12) и что с ней стало:

участок было стало
два цикла по тайлам (30 + 10 позиций) 9 852 132
pop_loose_mob_tick (обход 14 слотов) 12 090 996
check_loose_fall_on_kid (трамплин + обход) 5 868 546
вход + хвост 1 062 1 086
итого 28 872 2 760

Сделано двумя гейтами:

  • loose_any (статик pop_map.c) — «идёт ли анимация плит». Ставится в пяти местах записи ненулевой фазы, снимается САМИМ циклом по факту прохода, где не осталось ни одной живой фазы;
  • pop_mob_busy (резидент pop_state.c) — «занят ли хоть один слот падающего куска» (active или дочистка clean). Ставит mob_alloc, снимает обход по факту пустой таблицы. В резиденте, а не в pop_room.c, потому что читает его pop_map из банка 3.

Важно про границу: гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация». У лежащей плиты-потолка фаза 0, и крутить нечего — вопрос пользователя 2026-08-19. Асимметрия намеренная: ложная единица стоит одного холостого прохода, ложный ноль — застывшей навсегда плиты, поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту.

Покрытие: phys_loose_floor_breaks (взвод от шага и сотрясения) и новый phys_loose_gate_survives_room_change — на пятое место взвода (фаза восстановлена входом в комнату), которое не покрывал никто. Мутационная проверка: со снятым взводом тест падает.

P6. pop_process_trobs разложен [ЗАМЕР 2026-08-19] — 89 784

участок такты
вход + префетч кодов тайлов (маппинг окна 0) 11 058
цикл: два pop_torch_draw ~36 000
цикл: обход самих trob'ов ~43 000

Цена одного pop_pot_b (пламя факела) измерена отдельно, брейкпоинтами на резидентных адресах: 17 346 тактов, и это ЕДИНСТВЕННАЯ группа в распределении — то есть pop_pot_b в кадре зовут только два факела. При канвасе пламени 16×18 сами пиксели там 1 716, то есть 10 % цены; всё остальное — накладные (см. §1) плюс ~6 900 сверх pop_blit_b на самом pop_pot_b.

Направления:

  • P6a: pop_trob_modif(room) зовётся банковым вызовом на КАЖДЫЙ trob внутри цикла, хотя комната у них одна и та же — вынести наружу;
  • P6b: префетч 11 058 маппит окно 0 каждый кадр, а коды тайлов trob'ов меняются редко — кэшировать с инвалидацией по смене тайла/комнаты;
  • P6c: цена факела — это цена блита, то есть позиция P4.

Новое, найдено 2026-08-19.

P7. G5. Раскол draw_tile на узкие части [оценка дока: −50 000 … 60 000]

Диспетчер + контекст оплачиваются целиком всегда; у оригинала это девять независимых функций. В 11/15 это те самые 56 200 на один тайл — но если сделан P1, draw_tile чомпера вообще не вызывается, и здесь эффект пропадёт. Ценность приёма — в ДРУГИХ сценах (13/23, любая комната с плитами).

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

P8. HEAL-WIDTH — ширина heal'ов по фактическому следу [оценка: 5-6 % цены heal'ов]

ОБЯЗАТЕЛЬНАЯ по решению пользователя (2026-08-18). След плиты 58 px в подземелье / 57 во дворце против используемых 60 и 64. Постановка — TASKS_OPEN.md, якорь heal-width.

P9. G8 — пометку СОСЕДА ставить узкой полосой (28 px), а не тайлом [гипотеза]

Парная к HEAL-WIDTH. Относится к сценам с падающими плитами (13/23), в 11/15 не играет. Разбор — perf_green_phase.md §G8, там же три условия, из-за которых это не «просто уменьшить число».

P10. Футпринт персонажа — из физики, а не считать заново [замер: −11 574 на fore-проход]

backlog §1. С двумя персонажами — ~23 000 за кадр. Мешает то, что физика (банк 3) держит char_col_left/right в статиках, а слой фона — банк 2. Риск средний: окно fore-клипа заводилось под клинок и брызги.

P11. Мелочи с известной ценой [замер, backlog §7]

что цена где
pop_clip_char_top — трамплин банк 4 → банк 3 ради одной проверки 8 892 pop_cdraw.c
cd_sig_make + возврат из pop_char_draw 7 944 pop_cdraw.c
pop_loadkid + расчёт координат кадра 7 410 pop_cdraw.c
obj_x * 8 / 7 — последнее __divsint в горячем пути ~2 400 pop_char_draw

P12. G9 — снять временную оснастку [замер: −6 000]

pop_dbg_b1..b6 в pop_blit_b (~400 на блит), pop_dbg_kind/m16, pop_dbg_m5..m15, счётчик rd_cnt в pop_redraw_needed. Только ПОСЛЕ окончания оптимизации — без них не мерить.

P13. Крупные рефакторинги — брать, только если понадобится ещё запас

  • C7 / backlog §5-6: objtable + отложенные таблицы back/mid/fore. У оригинала «посетить тайл» стоит копейки, потому что таблицы только копят записи, а рисует один draw_table() в конце. У нас блит идёт сразу из обхода, и fore-проход отдельный НА КАЖДОГО персонажа.
  • backlog §4: единый проход по тайлам вместо трёх (pop_redraw_needed, pop_process_trobs, pop_fore_over_char) и семь счётчиков причин перерисовки вместо одного kind.
  • G6: меньше блитов в RD_FLOOR, G7: mob_tick_one в file-scope.

4. ПЛАН РАБОТ — состояние между сессиями

Рабочий чеклист. Правило: одна позиция = один заход = один коммит с замером до/после на сцене 11/15. Замер обязателен даже когда «очевидно» — из шести сделанных позиций две дали не то, что ожидалось.

# что статус факт
P5 loose_tick: гейты холостого хода сделано 2026-08-19 −33 840 на кадре (ждали −28 000)
P2 замер синей фазы замерено 2026-08-19 гипотеза «трамплины» отвергнута
P6 замер process_trobs замерено 2026-08-19 11 058 префетч + ~43 000 цикл
P1 чомпер под факелом: только слой anim следующее ждём 160 000
P2b гейт луча видимости стража ждём −30 000, риск низкий
P4 накладные блита (4 правки) ждём 28 000
P11 мелочи с известной ценой ждём 26 000
P10 футпринт персонажа из физики ждём 23 000
P6a/P6b trob_modif из цикла, кэш префетча ждём 20 000
P2a разбор pop_phys_tick (105 246 на двоих) цена известна, причина нет
P7 раскол draw_tile (G5) для 13/23, не для 11/15
P8/P9 HEAL-WIDTH + G8 обязательная для сцен с плитами
P12 снять оснастку ПОСЛЕДНЕЙ без неё не мерить

Достижим ли период 3 растра — арифметика после замеров

Работа сейчас 767 928, цель 430 000, снять надо 338 000. Что известно и посчитано:

источник ожидание
P1 (чомпер: только слой anim) 160 000
P3 (Кид перестанет будиться, следствие P1) 70 000
P2b (гейт луча видимости) 30 000
P4 (накладные блита) 28 000
P11 (мелочи) 26 000
P10 (футпринт из физики) 23 000
P6a+P6b 20 000
сумма 357 000

То есть цель достижима, но впритык и только если сделать ВСЁ перечисленное, причём половина суммы держится на одной позиции (P1). Запаса нет, и любая позиция, давшая меньше ожидаемого, возвращает нас к разбору pop_phys_tick (P2a) — 105 246 тактов на физику двух НЕПОДВИЖНЫХ персонажей, где цена измерена, а причина ещё не найдена.

Текущее состояние бюджета 11/15:

работа синяя зелёная циан период
до оптимизации 801 768 293 238 320 916 187 758 4 растра
после P5 767 928 285 864 294 384 187 764 4 растра

Цель — уложить РАБОТУ в 430 000 (тогда период станет 3 растра вместо 4; хвост кадра — три gfx_wait_vsync). Осталось снять 338 000.

Как воспроизвести сцену (важно для следующей сессии)

Сборка стартует прямо в ней: make (дефолты LEVEL=11 ROOM=15 POS=2) → make hddполный рестарт MAME (chdman -f даёт новый inode, memory mame_hdd_rebuild_restart) → в DSS набрать d: и roomtest. Кид встаёт в (0,2) лицом к чомперу, справа факел и страж — та самая сцена замеров. Штатный старт уровня возвращается через make ROOM=.

Проверка, что программа ЖИВА, обязательна перед любым чтением памяти: cur_room (0x97AA) должен лежать в 1..24 — на этом уже был сорван один замер (прочитаны два случайных байта остановленной машины).

Метод замера

Зонды — out (_io_border) в roomtest.c (база модуля 0x42AD) плюс резидентные пустышки pop_dbg_m* из pop_state.c. Адреса брать ЗАНОВО из .sprinter-cc-roomtest/roomtest.map после каждой пересборки. Скрипты сессии: perfrun.py <out> <сек> tag=addr ... и parseseq.py <файл> ПОСЛЕД.

Цену отдельной РЕЗИДЕНТНОЙ функции можно снять вообще без пересборки: bpset <вход>,1,{temp0=totalcycles; g} плюс bpset <точка>,1,{printf "… %d",totalcycles-temp0; g}. Так разложен блит в §1.


5. Сводка: что сколько даёт в 11/15

# приём эффект тип оценки риск
P1 чомпер: только anim-слой 160 000 модель низкий
P3 Кид перестанет будиться 70 000 модель следствие P1
P2 логика двух Char 40 000 … 70 000 гипотеза ?
P4 накладные блита (4 правки) 28 000 модель низкий
P5 loose_tick без плит 33 840 ФАКТ сделано
P6 цикл process_trobs 20 000 … 40 000 гипотеза ?
P10 футпринт из физики 23 000 замер средний
P11 мелочи (4 штуки) 26 000 замер низкий
P12 снять оснастку 6 000 замер нулевой
P7 раскол draw_tile 0 здесь (50 000 в 13/23) оценка средний
P8/P9 HEAL-WIDTH / G8 0 здесь (сцены с плитами) оценка низкий/средний

Верхняя часть списка (P1 + P3 + P4 + P5) — около 286 000 из 801 768, то есть 36 % работы кадра, и вся она низкого риска. Этого хватит, чтобы сцена ушла с 1,86 растрового кадра до ~1,2 — но НЕ хватит, чтобы период кадра упал с 4 растров до 3: для этого работа должна уложиться в 430 000, то есть нужны ещё ~90 000 сверху (P2 или P6).