Записаны, чтобы не повторять, и с разбором причины. 1. cd_sig_same блоком (сравнение 10 байт циклом вместо 13 сравнений полей): по листингу короче (1939 -> 1290), на машине хуже 438 978 -> 450 426. Сумма тактов по листингу считает инструкцию один раз, а тело цикла исполняется десять раз. 2. cd_touch_pb (пометка «для блита» из file-scope вместо четырёх аргументов): 438 978 -> 442 242. В зелёной фазе блиты идут пакетным путём, где нужны все четыре значения, а в регистрах они дешевле, чем чтение из статиков. 3. Обёртка pop_cd_hit_slot поверх pop_cd_hit — 1799 против 1318 тактов; помогло только когда сравнение переехало внутрь. Общий урок записан там же: короткий листинг не равно быстрый код, и снятие аргументов со стека помогает не всегда. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
47 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. Чомпер: перерисовка неизменной позы ✅ СДЕЛАНО 2026-08-19 — −110 802
Диагноз, с которым позиция заводилась, оказался неполным. Я приписал
190 260 тактов пометке от факела; зонд pop_dbg_kind показал, что все 312
перерисовок прогона — вид POP_RD_CHOMP (полная), а пометку соседа она
просто перебивала. Настоящая причина нашлась сверкой с animate_chomper
(seg007:0448): оригинал перерисовывает чомпер только при фазе < 6, пять
кадров из пятнадцати, потому что с фазы 5 поза не меняется
(chomper_fram1 = {3,2,0,1,4,3,3}). Мы метили тайл каждый кадр.
Сделано три вещи: условие фазы (с пометкой обеих страниц на фазе 5), новый
вид POP_RD_CHOMP_ANIM → pop_chomp_anim_draw (три блита поверх огня, порт
ветки redraw_frames_anim) и приоритет полной перерисовки над anim в
pop_set_redraw. Обе половины работают: замер даёт 40 % полных
перерисовок и 60 % лёгких.
Работа 768 684 → 657 882 (медиана), зелёная 294 510 → 183 420.
В 40 % кадров цена осталась прежней — там поза действительно меняется, и это
уже честная работа; резать её дальше только через P7 (раскол draw_tile)
или P8 (ширина heal).
Полный разбор — perf_l11_room15.md §6.
Исходная (неполная) постановка
Разбор в perf_l11_room15.md §3. Сейчас пометка от
факела обрабатывается как heal 32×64 + полный draw_tile (190 260 тактов на
единственный перерисованный тайл кадра); оригинал в этом случае рисует
ТОЛЬКО draw_tile_anim — графику чомпера поверх свежего пламени.
Останется 1-2 блита челюстей ≈ 20 000-33 000. Риск низкий: это сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку «фон трогали» вокруг тайла чомпера — см. P3.
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 честно выходят сразу.
Настоящие статьи — три:
- Физика двух Char — 105 246 (61 266 + 43 980), при том что оба
персонажа СТОЯТ и кадр позы не меняется. Это 13,7 % работы кадра и
самая крупная статья синей. Нужен ещё один уровень разбора — внутрь
pop_phys_tick(позиция P2a, отдельным заходом). - Луч видимости стража — до 37 032 (вместе с
pop_frame_timers, но тот заведомо копеечный: три счётчика). Считается КАЖДЫЙ кадр, хотя ни Кид, ни страж не сдвинулись. Кандидат на гейт «пересчитывать только при смене позиции или комнаты любого из двоих» — позиция P2b, ожидание −30 000, риск низкий. - heal двух Char — 67 734. Отдельной правки не требует: он платится
ровно потому, что
skip_maskникого не пропускает, и уйдёт вместе с P1/P3.
Новое, найдено 2026-08-19.
P15. Точность метки «фон трогали» ✅ СДЕЛАНО 2026-08-19 — −163 746 / −140 871
Постановка пользователя: не перерисовывать стража, пока он не двигается.
Две правки, и вторая оказалась решающей:
- метка: вместо «маска колонок × три ряда по 63 px» — диапазон y на
каждую колонку (
ymin/ymax, 40 байт на обе страницы). Прежняя гранулярность склеивала пламя факела (y 33..50) с клинком стоящего стража (y 59..65), между которыми девять пикселей зазора; - проверка:
cd_quietсверяет спрайт и накладной (клинок, брызги) ДВУМЯ отдельными прямоугольниками вместо объединённого bbox. Объединение включает пустой угол: спрайт стража в колонке 8, клинок уходит в колонку 7 на y 59..65, пламя метит колонку 7 на y 33..50 — и прямоугольник «спрайт + клинок» цеплял метку углом.
Без второй правки первая дала почти ноль (632 676 против 628 542 до неё) — это стоит помнить: точность структуры бесполезна, пока запрос к ней остаётся грубым.
| лёгкая позиция | тяжёлая позиция | |
|---|---|---|
| синяя | 259 050 → 223 902 | 257 520 → 245 808 |
| зелёная | 181 494 → 181 494 | 180 870 → 181 761 |
| циан | 194 262 → 59 406 | 319 842 → 192 090 |
| работа | 628 542 → 464 796 | 758 358 → 617 487 |
В тяжёлой позиции вдобавок исчезли пятирастровые кадры (было 27 %).
Проверено в MAME: статика чистая, динамика (пробежка, бой, переход в соседнюю комнату) без хвостов и просвечивания; хост-тесты зелёные.
Побочно исправлены два собственных дефекта первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0 (после
pop_cd_clear(0) метка второй переставала расти), и отсутствовала явная
инициализация — пустая колонка обозначается ymin = 255, а нули от crt0
читались бы как «затронута строка 0».
P16. Цианные проверки ✅ СДЕЛАНО 2026-08-19 — −25 818
Раскладка остатка цианной фазы (59 406) показала, что 47 883 из них — три вызова, а не отрисовка:
| вызов | было | стало |
|---|---|---|
pop_loose_mob_draw |
978 | 978 (гейт mobs_live работает) |
guard_over_kid |
14 424 | 0 |
pop_char_skip_mask |
28 605 | ~21 000 |
Три правки:
cd_sig_same— сравнение снимка БЕЗ построения структуры.cd_sig_makeзаписывал тринадцать полей в стековый кадр (через-n(ix)), и лишь потом шёл побайтовый цикл; теперь сравнение идёт прямо с источником и выходит на первом расхождении. −8 016;guard_over_kidпо условию — вопрос «кто поверх кого» не имеет смысла, когда не рисуется никто. Вызов перенесён ПОСЛЕskip_maskи идёт только приskip != 3. −14 118;pop_cd_hit_slot— проверка «задет ли слот» брала пять аргументов, три из них стеком (45 % тактов на IX). Теперь координаты берутся изpop_cd, а сравнение вынесено вhit_rectс file-scope аргументами. −3 684.
Отрицательный результат внутри третьей правки (не повторять): первая
версия была обёрткой, которая внутри всё равно звала pop_cd_hit с пятью
аргументами — стало ХУЖЕ (1799 тактов Z80 вместо 1318). Снимать аргументы
со стека надо у того, кто их читает, а не этажом выше.
Отрицательные результаты 2026-08-19 — НЕ ПОВТОРЯТЬ
Три попытки подряд сделали ХУЖЕ. Общая ошибка в двух из них — я оценивал правку по СУММЕ ТАКТОВ ИНСТРУКЦИЙ в листинге, а не по реально исполняемому пути.
1. cd_sig_same блоком вместо тринадцати сравнений. Снимок был
переложен так, чтобы сравнивать непрерывные 10 байт начала pop_char_t
циклом do { if (*a++ != *b++) return 0; } while (--i). По листингу
функция стала короче (1939 → 1290 тактов), а на машине стало хуже:
438 978 → 450 426 (+11 448).
Причина: сумма по листингу считает каждую инструкцию ОДИН раз, а тело цикла исполняется ДЕСЯТЬ раз. Тринадцать линейных сравнений выполняются по разу каждое и выходят раньше на первом же расхождении. Урок: короткий листинг ≠ быстрый код; цикл надо разворачивать в уме.
2. cd_touch_pb — пометка «для блита» из file-scope. pop_cd_touch
зовётся из pop_blit_b с четырьмя аргументами, хотя тот держит те же
значения в pb_x/pb_top/pb_w/pb_h. Специализированный вход без
аргументов дал 438 978 → 442 242 (+3 264).
Причина: в зелёной фазе блиты идут ПАКЕТНЫМ путём (draw_tile открывает
pop_cd_batch), а там нужны все четыре значения сразу — и в регистрах
(x, y приходят в HL/DE) они дешевле, чем чтение из статиков.
Снятие аргументов со стека помогает не всегда: если значение и так живёт
в регистре, статик его туда ещё и загружать заставит.
3. Обёртка pop_cd_hit_slot поверх pop_cd_hit (описана в P16):
внутри всё равно звала функцию с пятью аргументами и добавила свои — стало
1799 тактов вместо 1318. Помогло только когда сравнение переехало внутрь.
P14. Fore-проход персонажа — от 4 122 до 117 570 [замеры 2026-08-19]
Самая НЕСТАБИЛЬНАЯ статья кадра. Замеры на одной и той же сцене:
| ситуация | fore-проход |
|---|---|
персонаж пропущен (skip) |
4 122 |
| стоящий страж | 62 778 |
| живой Кид у чомпера | ~89 000 |
| труп Кида в челюстях | 117 570 |
Растёт от двух вещей: ширины футпринта (широкий кадр смерти, вынутый меч добавляет колонку) и числа тайлов с передним слоем внутри футпринта (здесь чомпер со своими зубьями). Отсюда практический вывод: в бою проход будет ближе к сотне тысяч, чем к шестидесяти — кадры выпадов и ударов широкие.
Замер трупа сделан по просьбе пользователя. Сама по себе эта ситуация не игровая («когда Кид — труп, игры нет»), но именно она показала верхнюю границу цены.
pop_char_fore = два трамплина в банк 2 (pop_fore_set_clip +
pop_fore_over_char) плюс обход тайлов футпринта, в каждом fore_tile.
У Кида дороже, чем у стража, потому что в его футпринте лежит чомпер, а у
чомпера есть собственный передний слой (POP_CHOMP_FRAM_FOR), который
перерисовывается поверх персонажа каждый кадр.
Что можно пробовать, по возрастанию радикальности:
- слить два трамплина в один вызов (мелочь, ~4 000);
- P10 — брать футпринт из физики, а не считать заново (−11 574 на проход, то есть до −23 000 на двоих);
- гейт по сигнатуре: пропускать проход, если не изменились ни кадр персонажа, ни тайлы его футпринта. Это расхождение с оригиналом — он рисует foretable безусловно;
- P13 — objtable и отложенные таблицы: у оригинала «посетить тайл» стоит копейки именно потому, что таблицы только копят записи.
P3. Персонажи будятся каждый кадр ✅ ЧАСТИЧНО СБЫЛОСЬ
В лёгкой позиции — да: после P1 pop_char_draw(KID) стоит 204 такта,
метка от чомпера до Кида больше не дотягивается.
В тяжёлой позиции — нет: стоит Киду шагнуть на 7 пикселей вправо, и его
спрайт пересекается с тайлом чомпера и пламени, skip_mask перестаёт
пропускать, и он снова стоит ~144 000 (54 738 draw + ~89 000 fore). То
есть выигрыш P3 держится только пока персонаж не подошёл к анимированному
тайлу — а в игре он к нему подходит постоянно.
Исходная оценка (−70 000) была:
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. Крупные рефакторинги — брать, только если понадобится ещё запас
Чем P13 НЕ является (вопрос пользователя 2026-08-19). Это не «рисовать
комнату заново каждый кадр в скрытый буфер». Такой вариант исключён
арифметикой: 30 тайлов по 5-6 спрайтов при цене блита 16 674 (и 179 914 за
полную запечку одного тайла) дают порядка 3 000 000 тактов — семь
растровых кадров. Оригинал так тоже не делает: у него та же
инкрементальная схема с пометками (redraw_frames_full / _anim /
_fore), перерисовываются только помеченные тайлы.
Разница не в объёме отрисовки, а в цене ПОСЕЩЕНИЯ тайла: у нас
fore_tile(r, c) сразу блитит (со всеми 6 126 фиксированных накладных), а
у оригинала add_backtable/add_midtable/add_foretable только кладут
запись в массив, и рисует один draw_table() в конце. Плюс у него ОДИН
обход тайлов за кадр против наших трёх.
- 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. Замер обязателен даже когда «очевидно» — из семи закрытых позиций ТРИ дали не то, что ожидалось (P1 — вдвое меньше, P2a — почти ничего, таблицы порогов — регресс).
Закрыто
| # | что | факт |
|---|---|---|
| P15 | точность метки «фон трогали» + раздельная проверка клинка | −163 746 лёгкая / −140 871 тяжёлая |
| P16 | цианные проверки: снимок без структуры, guard_over_kid по условию, hit_slot без пяти аргументов |
−25 818 |
| P5 | loose_tick: гейты холостого хода |
−33 840 (ждали −28 000) |
| P1 | чомпер: перерисовка только при фазе < 6 | −110 802 (ждали −160 000) |
| P2b | луч видимости: колонки + один банковый вызов | −26 448 (ждали −30 000) |
| P2a | coll_scan в 8 бит + снят с IX |
−2 892 (крупной статьи в физике нет) |
| P3 | Кид перестал будиться каждый кадр | сбылось само после P1 — но только в ЛЁГКОЙ позиции |
| P2/P6 | замеры синей фазы и process_trobs |
гипотеза «трамплины на спецсобытиях» отвергнута |
| — | замер цианной фазы | крупного лишнего в отрисовке персонажа нет |
Осталось, по убыванию ожидаемого эффекта
| # | что | ожидание | риск | комментарий |
|---|
| P14 | fore-проход персонажа — 62 778 стоящий страж … 117 570 широкий кадр | до −80 000 | высокий | самая нестабильная статья. Включает P10 как первый шаг |
| P4 | накладные блита (4 правки) | −28 000 | низкий | 6 126 фиксированных на любой блит |
| P11 | мелочи с известной ценой | −26 000 | низкий | clip_char_top 8 658 подтверждён замером |
| P10 | футпринт персонажа из физики | −23 000 | средний | часть P14 |
| P6a/P6b | trob_modif из цикла, кэш префетча | −20 000 | низкий | тот же паттерн трамплина в цикле |
| P7 | раскол draw_tile (G5) | −50 000 в 13/23 | средний | в 11/15 не играет |
| P8/P9 | HEAL-WIDTH + G8 | 5-6 % цены heal'ов | низкий/средний | обязательная по решению пользователя; для сцен с плитами |
| P13 | objtable + отложенные таблицы, единый проход по тайлам | не оценено | очень высокий | большой рефакторинг слоя фона |
| P12 | снять оснастку | −6 000 | нулевой | последней: без неё не мерить |
Текущее состояние бюджета
| работа | синяя | зелёная | циан | период | |
|---|---|---|---|---|---|
| до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра |
| после P5 | 767 928 | 285 864 | 294 384 | 187 764 | 4 |
| после P1 (медиана) | 657 882 | 286 503 | 183 420 | 187 761 | 4 |
| после P2a | 654 990 | 283 215 | 183 798 | 187 812 | 4 |
| после P2b (лёгкая позиция) | 628 542 | 259 500 | 181 068 | 187 761 | 4 |
| ТЯЖЁЛАЯ позиция (Кид на шаг правее) | 758 358 | 257 520 | 180 870 | 319 842 | 4 и 5 |
| после P15, лёгкая | 464 796 | 223 902 | 181 494 | 59 406 | 4 |
| после P15, тяжёлая | 617 487 | 245 808 | 181 761 | 192 090 | 4 везде |
| после P16, лёгкая | 438 978 | 218 052 | 181 494 | 39 438 | 4 |
Итог восьми позиций: 801 768 → 464 796 в лёгкой позиции (−42 %) и 758 358 → 617 487 в тяжёлой (−19 %). Отдельно важно: в тяжёлой позиции исчезли пятирастровые кадры (было 27 %), период стал ровно 4 — рывки ушли.
Достижима ли цель — арифметика на 2026-08-19
Цель: работа ≤ 430 000, тогда период станет 3 растра (хвост кадра — три
gfx_wait_vsync).
- в ЛЁГКОЙ позиции снять надо 8 978 — одна правка;
- в ТЯЖЁЛОЙ — около 187 000 (замер после P16 не повторялся).
Всё оставшееся в списке, кроме P13, даёт по оценкам порядка 100 000 — и это оптимистично. Арифметика не сходится: сцена с двумя персонажами, чомпером и двумя факелами в три растра не укладывается без одного из трёх решений:
- P13 — переход на objtable и отложенные таблицы, как в оригинале (единственный резерв нужного размера, но это переписывание слоя фона);
- осознанное расхождение с оригиналом — например, не перерисовывать передний слой персонажа, пока не изменились ни персонаж, ни тайлы под ним (гейт по сигнатуре футпринта);
- принять 4 растра как рабочий режим для сцен такой плотности и выравнивать период, чтобы не было рывков 4/5.
Решение за пользователем — это выбор между точностью порта и скоростью.
Как воспроизвести сцену (важно для следующей сессии)
Сборка стартует прямо в ней: 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).
5б. Цианная фаза разложена [ЗАМЕР 2026-08-19]
Фаза 188 004 тактов, и она НЕ менялась ни от P1, ни от P5, ни от P2b.
| участок | такты |
|---|---|
check_mirror |
3 198 |
loose_mob_draw + guard_over_kid + skip_mask |
34 374 |
pop_char_draw(KID) |
204 |
соперник: char_draw + char_fore |
145 896 |
fore_needed + hp_draw |
2 598 |
char_fore(KID) + борта |
1 758 |
Кид уже пропускается — 204 такта, то есть надежда P3 всё-таки сбылась после P1: метка от чомпера до него больше не дотягивается. А страж перерисовывается каждый кадр, и это ЧЕСТНО: пламя правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально накрывает ему голову (пламя занимает y 5..22, страж 12..62).
Отрисовка стража (148 302) по частям:
| участок | такты | доля |
|---|---|---|
pop_char_fore (два трамплина в банк 2 + обход тайлов) |
62 778 | 42 % |
клинок: pop_sword_draw + cd_overlay_add + cd_clip_add |
27 522 | 19 % |
блит спрайта + clip_char_right |
20 982 | 14 % |
| загрузка кадра и геометрия | 12 696 | 9 % |
pop_clip_char_top (трамплин банк 4 → банк 3) |
8 658 | 6 % |
снимок прямоугольника + cd_clip_add |
7 890 | 5 % |
gfx_w0_unmap + cd_sig_make |
4 968 | 3 % |
вход + cd_heal |
2 946 | 2 % |
Вывод: крупного лишнего здесь нет. Единственная явно лишняя статья —
трамплин clip_char_top (8 658), и убрать его непросто: функции нужны
get_tile и таблицы деления из банка 3, а перенос в резидент вернёт тот же
трамплин внутрь. Всё остальное — работа, которую персонаж действительно
делает: рисует себя, клинок и передний слой поверх себя.
6. Иерархия референсов (уточнена 2026-08-19)
Сравнение трёх реализаций луча видимости показало, что источники не равноценны, и это важно для ЛЮБОЙ будущей оптимизации:
| источник | что берём | чего НЕ берём |
|---|---|---|
Apple II (Prince-of-Persia-Apple-II) |
как это делается на 8 битах: таблицы вместо делений, борьба за такты | ничего — но код на 6502, читать сложнее |
| SDLPoP | эталон ПОВЕДЕНИЯ (декомпиляция DOS-версии) | реализацию: она нарочно «расслаблена» под 32 бита |
| mininim | разбор краевых случаев, второе мнение о замысле | алгоритмы — переписан с нуля, механика местами своя |
Доказательство на конкретном месте: get_tile_div_mod в SDLPoP содержит
комментарий
// DOS PoP does this:
// obj_xl = tile_mod_tbl[xpos];
// return tile_div_tbl[xpos];
а вместо этого делает x % TILE_SIZEX и x / TILE_SIZEX. Таблицы в файле
лежат, но нужны только для эмуляции чтения DOS-версии ЗА ГРАНИЦЕЙ массива.
Apple II (CTRLSUBS.S, GETBLOCKX) читает ровно BlockTable[x].
Правило: сверять поведение по SDLPoP, а реализацию под 8 бит — по Apple II и по комментариям вида «DOS PoP does this» в самом SDLPoP.
7. Повторяющийся источник цены: банковый трамплин в цикле
Уже трижды крупнейшей статьёй оказывался не алгоритм, а вызов __banked-
функции ИЗ ЦИКЛА, идущего в другом банке:
| место | цена | лечение |
|---|---|---|
луч видимости: pop_tile_at по колонке (P2b) |
36 786 → 13 002 | один вызов на весь отрезок |
pop_clip_char_top — банк 4 → банк 3 ради одной проверки |
8 892 | не сделано (P11) |
pop_trob_modif(room) на каждый trob в цикле |
не мерено | не сделано (P6a) |
Что проверять в первую очередь при новом «дорогом» месте: не сколько там арифметики, а сколько раз за кадр пересекается граница банка.