Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где не анимируется ни одна плита и не летит ни один кусок. Раскладка зондами m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск куска над головой Кида 5 868 (там ещё и банковый трамплин). Два гейта: loose_any (статик pop_map.c) — «идёт ли анимация плит». Ставят пять мест записи ненулевой фазы: make_loose_fall, ветка потолка в check_press, do_knock для обоих рядов и восстановление фазы из room_modif при входе в комнату. Снимает его сам цикл, по факту прохода, в котором не осталось ни одной живой фазы. pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска» (active или дочистка clean). Ставит mob_alloc, снимает обход по факту пустой таблицы. В резиденте, а не в pop_room.c, потому что читает его pop_map из банка 3, а писучие статики банкового модуля наружу не видны. Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя). Асимметрия намеренная — ложная единица стоит одного холостого прохода, ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту пустого прохода. Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760. На кадре работа 801 768 -> 767 928. Ожидание по реестру было -28 000. Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода (фаза восстановлена входом в комнату) — единственное, которое не прогонял ни один тест, и дающее самый тихий отказ. Мутационная проверка: со снятым взводом тест падает (фаза 3 вместо 4). Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2 (дефолт), roomtest стартует в 11/15 с Кидом в (0,2). kid_init ставит x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё физика относила Кида в тайл чомпера, и он погибал на старте (найдено пользователем). Сдвиг внутрь на 2: колонку определяет весовая точка кадра, поэтому число снято замером, а не выведено геометрией. План работ между сессиями — docs/perf_registry.md §4: очередь позиций со статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
Реестр оптимизаций: всё отложенное, в одном списке
Собрано 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. Логика двух Char — 152 190 тактов БЕЗ единого блита [гипотеза: −40 000 … −70 000]
m1→C: kid_tick, pop_phys_tick, pop_guard_tick, pop_guard_phys_tick,
боёвка, pop_check_can_guard_see_kid. Графики там нет вообще, а стоит это
19 % работы кадра — столько же, сколько вся отрисовка обоих персонажей.
Сначала замер (зонды внутрь синей фазы, нужна пересборка), потом решение.
Кандидаты по прошлым находкам: sdcc_z80_stack_locals_hot_loop (локали в
IX-фрейме), деления в луче видимости, банковые трамплины на каждый шаг.
Новое, найдено 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 — 59 100 без блитов пламени [гипотеза: −20 000 … −40 000]
Префетч кодов тайлов делает pop_level_access_begin/end (маппинг окна 0)
каждый кадр; дальше switch по всем trob'ам комнаты. Надо померить, сколько
там trob'ов и куда уходит время.
Новое, найдено 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 | замер логики двух Char | ⬜ следующее | нужна пересборка с зондами |
| P6 | замер цикла process_trobs |
⬜ вместе с P2 | та же пересборка |
| P1 | чомпер под факелом: только слой anim | ⬜ | ждём −160 000 |
| P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 |
| P11 | мелочи с известной ценой | ⬜ | ждём −26 000 |
| P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 |
| P7 | раскол draw_tile (G5) |
⬜ | для 13/23, не для 11/15 |
| P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами |
| P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить |
Текущее состояние бюджета 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).