Files
Sprinter-SDCC/applications/PoP/docs/perf_backlog.md
T
snark13 c8fe0bd37a pop_blit_b: быстрый путь без клипа + backlog отложенной оптимизации
Клипованный путь вынесен в отдельную функцию 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>
2026-08-10 11:05:42 +03:00

11 KiB
Raw Blame History

Оптимизация отрисовки — что НЕ сделано (замеры на 2026-08-10)

Список отложенных идей с измеренной ценой. Всё измерено брейкпоинтами в MAME (z80_profiling_method) на роомтесте, уровень 1 комната 1.

Прежде чем брать что-то отсюда — перечитать «Как мерить» ниже: половина прошлых гипотез не подтвердилась, и подтвердились не те, что казались очевидными.

Как мерить (иначе цифры не сходятся)

  • Такт totalcycles ≠ номинальный T-такт Z80. У ОЗУ Sprinter wait-state'ы, замеренная стоимость ≈ 2,4× справочной (get_tile: 574 против 1 422). Считать по таблице тактов нельзя. Подробности — memory sprinter_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_bpop_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).