Клипованный путь вынесен в отдельную функцию blit_b_clip: под его девять 16-битных локалей SDCC заводит кадр IX, и за этот кадр платили ВСЕ блиты фона, включая те, где клипа нет вовсе (весь фон вне fore-прохода — факелы, зелья, перерисовка тайлов, у них pop_t_fclip_on == 0). Быстрый путь идёт сразу в gfx_blit_noclip. Замер: pop_torch_draw 41 778 -> 31 218 тактов на факел (часть разницы — прошлая правка pop_cd_touch; чистый вклад этой ~6 300 на блит). Поведение не изменилось: клипованная ветка перенесена дословно. docs/perf_backlog.md — отложенные идеи с измеренной ценой (футпринт из физики 11 574, размеры ленты из каталога атласа, один map/unmap на группу блитов, единый проход по тайлам как redraw_needed_tiles, objtable, отложенные таблицы back/mid/fore) плюс раздел «как мерить»: wait-state'ы дают 2,4x к справочным тактам, кадр 430 000, адреса символов меняются после каждой пересборки, сцена между сессиями не воспроизводится. Там же — что уже проверено и НЕ сработало. tests-host: 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
Оптимизация отрисовки — что НЕ сделано (замеры на 2026-08-10)
Список отложенных идей с измеренной ценой. Всё измерено брейкпоинтами в
MAME (z80_profiling_method) на роомтесте, уровень 1 комната 1.
Прежде чем брать что-то отсюда — перечитать «Как мерить» ниже: половина прошлых гипотез не подтвердилась, и подтвердились не те, что казались очевидными.
Как мерить (иначе цифры не сходятся)
- Такт
totalcycles≠ номинальный T-такт Z80. У ОЗУ Sprinter wait-state'ы, замеренная стоимость ≈ 2,4× справочной (get_tile: 574 против 1 422). Считать по таблице тактов нельзя. Подробности — memorysprinter_wait_states_2x. - Растровый кадр = 430 000 тактов. Главный цикл спейсится тремя
gfx_wait_vsync, поэтому работа сверх 430 000 стоит СРАЗУ целый лишний кадр. Граница дискретная: 3 растровых кадра на логический или 4. - Адреса символов меняются после КАЖДОЙ пересборки (
roomtest.map,bank*_*.sym). Маркер со старым адресом молча не срабатывает, и разбивка выглядит правдоподобно, но врёт. - Сцена между сессиями не воспроизводится точно: позиция Кида до пикселя, состояние плиты (2,6), фаза факелов. Сравнивать «до/после» можно только по ОДНОЙ функции с одинаковыми входами, а не по общей работе за кадр.
- Кто делит: брейкпоинт на
__divsint/__divuint/__divucharс печатью адреса возврата —bpset <addr>,1,{printf "ret=%04X\n",w@(sp); g}. - Трасса вызовов графики с параметрами: скрипт в истории сессии, ставит
маркеры фаз на трамплин
___sdcc_bcall_ehl(условиеhl==<адрес>&&(de&0xff)==<банк>) и брейкпоинты на листья libbgi с печатью аргументов (__sdcccall(1): arg1 = HL, arg2 = DE, дальше стек с sp+2).
Профиль на 2026-08-10
Сцена: комната 1, два факела, стража нет.
| сцена | работа за кадр | период |
|---|---|---|
| Кид в покое, факел не задет (пропуск работает) | 244 026 (57 %) | 3 кадра |
| Кид стоит на факеле (перерисовывается каждый кадр) | 366 240 (85 %) | 3 кадра |
| Кид в щебне (2,4), движется | ~357 000 (83 %) | 3 кадра |
| Кид (0,5) в движении | 421 254 (98 %) | 4 кадра |
Одна перерисовка персонажа = ~137 000 тактов = 32 % растрового кадра, из них полезной работы (heal 20×19 + спрайт 12×41) — меньше трети.
1. Футпринт персонажа — брать из физики, а не считать заново
Цена: 11 574 такта на каждый fore-проход (от входа в char_footprint до
первого fore_tile).
redraw_at_char (seg003:0430) берёт ГОТОВЫЕ char_col_left/right,
char_top_row, char_bottom_row — их в этом же кадре посчитала физика
(set_char_collision, seg006:0723). У нас char_footprint (pop_bg.c)
считает их заново внутри fore-прохода.
Мешает то, что физика (банк 3) держит их в статиках, а слой фона — банк 2.
Надо опубликовать их так же, как уже опубликованы pop_cd[who].fpx/fpy/fpw/fph.
Заодно: оригинал расширяет футпринт ТОЛЬКО на одну колонку при вынутом
мече и объединяет с футпринтом ПРОШЛОГО кадра (prev_char_col_left/right).
Мы вместо этого расширяем окном fore-клипа и посещаем 6 тайлов там, где
оригинал посетил бы 4. Разница видна в замере: fore-проход стоит 58 764
там, где реально рисует, и 112 758 там, где не рисует ничего — вся
разница в числе посещённых тайлов.
Осторожно: окно клипа заводилось под клинок и брызги (они уходят вперёд-вверх за габарит кадра). Менять — с прогоном боя и падений.
2. Размеры ленты — из каталога атласа, а не через окно 0
Цена: ~750 тактов на atlas_image + часть из 6 396 на «чтение w/h и
арифметика клипа», на КАЖДЫЙ блит фона.
Сейчас pop_blit_b, чтобы узнать размер куска, зовёт atlas_image (тот
мапит страницу в W3, читает запись каталога, возвращает W3 назад), потом
gfx_w0_map и читает w/h из шапки ленты.
А размеры уже лежат в каталоге: запись 8 байт — offset u16, fw u8, fh u8, nx u8, ny u8, резерв u16, и у всех фоновых лент nx = ny = 1, то
есть fw/fh в точности равны w/h из шапки (проверено по
pop_env0.atl). atlas_image их читает и выбрасывает.
Вариант A (0 байт памяти): atlas_image_wh() рядом с atlas_image —
вернуть заодно размер.
Вариант B (без маппинга вовсе): снять каталоги при загрузке в резидентную
таблицу. Объём: фон (env0-4 + wall + fore + pot) = 313 лент, по 2 байта
= 626 Б; всё вместе с Кидом и стражем = 600 лент = 1200 Б. Свободной
кучи на 2026-08-10 — 2873 Б.
Ожидаемый выигрыш скромный: ~2 000–3 000 из ~16 000 накладных на блит.
3. Один gfx_w0_map/unmap на группу блитов
Цена: ~5 500 тактов на блит (unmap плюс возвраты по цепочке
pop_pot_b → pop_blit_b → трамплин).
Куски одного прохода часто лежат на одной странице атласа, а мапим и
размапливаем на каждый. Мешает то, что pop_blit_b — общий лист для всех
вызывающих; нужна форма «открыть страницу, N блитов, закрыть».
4. Единый проход по тайлам вместо трёх
У оригинала за кадр ОДИН обход тайлов — redraw_needed_tiles (seg008):
контекст тайла (curr_tile, curr_modifier, draw_xh, draw_main_y)
ставится по разу на тайл в load_curr_and_left_tile, а redraw_needed
смотрит семь независимых счётчиков (wipe_frames, redraw_frames_full,
redraw_frames_anim, redraw_frames2, redraw_frames_floor_overlay,
redraw_frames_fore, tile_object_redraw) и делает только помеченное.
У нас три обхода: pop_redraw_needed, pop_process_trobs и
pop_fore_over_char. Плюс один kind на тайл вместо семи счётчиков — две
разные причины перерисовки одного тайла конфликтуют.
Это большой рефакторинг всего слоя фона; браться только если понадобится ещё заметный запас.
5. objtable: персонажи, привязанные к тайлу
Оригинал кладёт персонажей в objtable и рисует их в
draw_objtable_items_at_tile(tilepos) во время обхода тайлов — порядок
окклюзии получается сам. У нас отдельный fore-проход НА КАЖДОГО персонажа.
Со вторым персонажем (страж) цена удваивается.
6. Отложенные таблицы back/mid/fore
add_backtable/add_midtable/add_foretable только КЛАДУТ запись в массив,
рисование — один draw_table() в конце. Поэтому «посетить тайл» у
оригинала стоит копейки. У нас блит идёт сразу из обхода.
7. Мелочи с известной ценой
| что | цена | где |
|---|---|---|
pop_clip_char_top — трамплин банк 4 → банк 3 ради одной проверки тайла над головой |
8 892 | pop_cdraw.c / pop_map.c |
pop_loose_tick при полном отсутствии падающих плит в комнате |
27 438 | pop_map.c |
obj_x * 8 / 7 — единственное оставшееся __divsint в горячем пути |
~2 400 | pop_char_draw |
cd_sig_make + возврат из pop_char_draw |
7 944 | pop_cdraw.c |
pop_loadkid + расчёт координат кадра |
7 410 | pop_cdraw.c |
Что уже проверено и НЕ сработало
- Маска «у тайла есть передний слой» (
FORE_ANY) + контекст тайла один раз. Сделано (коммитa9f4521), эффект нулевой: в футпринте Кида тайлы почти всегда С передним слоем, а снятое второе чтение кода съедено проверкой маски. Оставлено как сближение с оригиналом. - «Быстрый путь для окна коллизии целиком внутри комнаты». Не срабатывал почти никогда: Кид в колонке 0 даёт окно с −1. Заменён на разбиение окна на непрерывные пробеги.
- Флаг «фон трогали» вместо позиционной метки — нулевой выигрыш,
факелы гасили пропуск для всех сразу (см.
pop_cdraw.h).