Files
Sprinter-SDCC/applications/PoP/docs/perf_backlog.md
T
snark13 ec3cca5e1e Луч видимости стража: колонка из таблицы вместо деления (Кид у шва)
tile_at_kid (guards.c) считала колонку честным / и %, хотя резидентная
POP_TILE_DIV — это и есть tile_div_tbl оригинала, и остальной порт давно на
неё переведён.  У SDCC z80 пара / и % над int это __divsint плюс __modsint,
который внутри снова зовёт __divsint — ~5 400 тактов на вызов.

Зовут её в ЦИКЛЕ по колонкам между стражем и Кидом
(check_can_guard_see_kid, seg003:761).  Когда Кид у шва, его curr_col = −1,
луч тянется через всю комнату: замер дал ВОСЕМЬ пар делений за кадр,
около 43 000 тактов = 10 % растрового кадра, в фазе логики.  После фикса
таких вызовов не остаётся.

Как ловилось: брейкпоинт на __divsint с печатью адреса возврата дал
ret=C033 восемь раз за кадр; остановка на нём с dasm при замапленном банке
показала HL−65 / ld de,#14 / call __divsint по смещению 0x24 банка 1.

Снята и ложная тревога из прошлого коммита: пролог pop_char_fore на шве НЕ
разбухает до 134 730 — это была ошибка зонда (адрес fore_tile в банке
совпадает с кодом других банков, в интервал попадали чужие срабатывания).
Чистый замер: пролог 16 950, как и в середине комнаты.  Урок записан в
«Как мерить» в docs/perf_backlog.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:41:53 +03:00

24 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}. Если адрес возврата в банке (>= 0xC000), поставить тот же брейкпоинт с условием w@(sp)==<адрес> и БЕЗ g: машина встанет с нужным банком в окне, и dasm покажет вызывающего.
  • Брейкпоинт по адресу в банке ловит ВСЕ банки. 0xC000..0xFFFF — общее окно, и один и тот же адрес есть у семи модулей сразу. Либо ловить через трамплин (hl==<адрес>&&(de&0xff)==<банк>), либо перепроверять, что срабатывания идут из нужной фазы: иначе в интервал попадает чужой код и цифры врут (так я намерил несуществующие 134 730 тактов в прологе pop_char_fore).
  • Трасса вызовов графики с параметрами: скрипт в истории сессии, ставит маркеры фаз на трамплин ___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) — меньше трети.

Профиль дворца (уровень 4) на 2026-08-10

Сцена: уровень 4, Кид НЕПОДВИЖНО стоит на (1,7) (комната с решёткой), стража нет. Разбивка одного логического кадра брейкпоинтами на границах фаз (адреса PROF() из roomtest.lst + базы _CODE = 0x42AD):

фаза тактов доля работы
ввод + читы + heal 59 340 11 %
логика (kid_tick, phys_tick, страж, боёвка) 85 356 16 %
pop_loose_tick 27 396 5 %
pop_process_trobs 106 107 21 %
pop_redraw_needed 447
шов / смена уровня / вспышка 5 448 1 %
guard_over_kid + pop_char_skip_mask 4 788 1 %
pop_char_draw(KID) 52 206 10 %
соперник + loose_mob_draw_over + hp_draw 4 086 1 %
pop_char_fore(KID) 171 693 33 %
pop_room_clip_borders + прочее 742
ИТОГО работа 517 609 120 % растрового кадра → период 4 кадра

По цветам бордюра: синий (PROF(2)) 144 696 (28 %), зелёный (PROF(4)) 139 398 (27 %), циан (PROF(6)) 233 515 (45 % работы = 54 % растрового кадра, начинается на 74 % первого кадра и кончается на 123 %).

СДЕЛАНО 2026-08-10: метка «фон трогали» стала маской ТАЙЛОВ

Было: один union-прямоугольник на страницу. Три факела трогают по пятну 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю комнату; Кид, стоящий где угодно между крайними факелами, в неё попадал и перерисовывался каждый кадр со всем fore-проходом.

Стало: uint16_t pop_cd_dmask[2][3] — бит на колонку, слово на ряд, набор на страницу. Колонка берётся сдвигом (x >> 5), ряд — цепочкой сравнений; проверка в cd_quiet — три AND через резидентный pop_cd_hit.

Гранулярность тайла — это гранулярность ОРИГИНАЛА: пометки там тоже по тайлам (redraw_frames_anim[tilepos], set_wipe), персонажи привязаны к тайлу через tile_object_redraw[tilepos], а единственное подтайловое уточнение (wipe_heights) — по высоте, не по ширине. Полутайл (16 px) не дал бы ничего: пламя рисуется с отступом 8 px и шириной 16, то есть занимает середину тайла и задевает обе половины.

Замер на той же сцене, где снимался профиль ниже (Кид неподвижно на (1,7)):

было стало
циан (спрайты + fore) 233 515 24 781
работа за кадр 517 609 306 553
период 4 растровых кадра 3

СДЕЛАНО 2026-08-10: деление в луче видимости стража (Кид у шва)

tile_at_kid (guards.c) считала колонку честным / и %, тогда как везде уже стоит резидентная таблица POP_TILE_DIV (это и есть tile_div_tbl оригинала). У SDCC z80 это __divsint плюс __modsint, а тот внутри снова зовёт __divsint — ~5 400 тактов на вызов.

Зовут её В ЦИКЛЕ по колонкам между стражем и Кидом (check_can_guard_see_kid, seg003:761). Когда Кид стоит У ШВА, его curr_col = 1, луч тянется через всю комнату, и за кадр набегало ВОСЕМЬ пар делений — около 43 000 тактов, 10 % растрового кадра, в фазе ЛОГИКИ.

Замер: брейкпоинт на __divsint с печатью адреса возврата дал ret=C033 восемь раз за кадр; остановка на нём и дизассемблирование с правильным банком показали HL65, ld de,#14, call __divsint по смещению 0x24 банка 1 — tile_at_kid. После фикса пар C033 не остаётся ни одной.

Заодно снята ложная тревога. В прошлом замере я записал, что на шве пролог pop_char_fore разбухает до 134 730 тактов. Это была ошибка зонда: брейкпоинт на fore_tile стоял по адресу, который совпадает с кодом ДРУГИХ банков, и в интервал попадали чужие срабатывания. Чистый замер (Кид в колонке 0, спрайт свисает за левый край, окно x 8..5): пролог 16 950 — ровно как в середине комнаты, весь fore-проход 41 803, pop_room_clip_borders 14 364. Урок в «Как мерить» выше: адрес в банке нужно либо проверять на уникальность, либо ловить через трамплин с условием на банк.


0. Дворцовая кладка в fore-проходе — СДЕЛАНО 2026-08-10

Все три шага выполнены; замер после — в конце пункта. Ниже сохранён исходный разбор: он объясняет, почему предфильтра fore_tile мало и откуда взялись габариты кусков.

Было: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей — ровно ноль.

Замер (уровень 4, Кид на (1,7)). Окно fore-клипа в этот момент — x 229..241, y 106..147 (прочитано из pop_t_fclip_* брейкпоинтом). pop_fore_over_char обходит 6 тайлов:

тайл тактов
(0,6) (0,7) (1,6) (1,7) 2 800 4 600 каждый
(2,6) — стена 74 310
(2,7) — стена 65 553

Ряд 2 этой комнаты — стена, и он лежит ПОД ногами Кида, то есть попадает в его fore-окно всегда. Внутри одного тайла стены wall_pattern_palace делает 6 × wpp_fill (≈ 3 100 каждый) + 5 × pop_wall_b (≈ 9 760 каждый) ≈ 60 000 тактов.

Ни один кусок в окно не попадает:

  • верхняя заливка стоит на dmy - 59 = 157, окно кончается на y = 147; все остальные куски ещё ниже;
  • тайл (2,6) занимает x 192..223, окно начинается с x = 229 — он промахивается и по горизонтали тоже.

Тайл всё равно проходит, потому что предфильтр в fore_tile (pop_bg.c, x0 < fclip_x1 && x0 + 40 > fclip_x0 && y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0) намеренно грубый — габарит 40×78 на тайл. Дальше wpp_fill честно режет по окну и выходит с пустым прямоугольником, но 3 100 тактов на арифметику клипа уже потрачены, а pop_wall_b о существовании окна не знает вовсе: он идёт в atlas_image + gfx_w0_map, читает w/h и только там обнаруживает, что рисовать нечего.

Что делать (по возрастанию объёма):

  1. Ранний выход из wall_pattern_palace: самый верхний пиксель узора — dmy - 59, самый нижний — dby + высота нижнего декаля. Один if (pop_t_fclip_on && (fclip_y1 <= dmy - 59 || fclip_y0 > dby + h)) return; плюс такая же проверка по x убивает оба тайла целиком почти даром.
  2. Прогнать pop_wall_b в этом узоре через ту же проверку окна, что уже есть у wpp_fill (нужны размеры кусков — см. п. 2 ниже, «размеры из каталога атласа»).
  3. Сузить сам предфильтр fore_tile до реального габарита узора вместо 40×78 — тогда лечится не только дворец.

Порядок в подземелье тот же, но дешевле: wall_pattern в подземелье делает до 3 блитов и ни одной заливки (~29 000 на тайл против ~60 000). Это же объясняет, почему после перехода на дворцовый тайлсет период вырос.

Сверено с SDLPoP (seg008.c:1943 wall_pattern, ветка !is_dungeon && GRAPHICS_VGA): состав узора у нас дословный — 5 add_wipetable + 4 декаля + нижняя заливка + нижний декаль. Расходимся не составом, а тем, что оригинал складывает всё в foretable и рисует одним draw_table(), у которого «посетить тайл» стоит копейки (см. п. 6).

Что сделано и сколько дало

  1. Ранний выход из wall_pattern_palace по окну fore-клипа (узор целиком в x [xh*8, xh*8+32), y [dmy59, dby]).
  2. Отсев каждого декаля (wp_blit) по реальному габариту вместо заведомо большего 64×64 в pop_blit_b. Размеры сняты из каталогов атласов: pal_wall.atl — группы 3..5 = 8×7, 6..8 и 9..11 = 32×12, 12..14 = 30×5, 15..17 = 32×3.
  3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит там выше — левая марка уходит на dby+POP_YOFF67) плюс wp_blit на RNDBLOCK (32×21), обоих разделителях (9×21) и обеих марках (pop_wall.atl: 16/17 = 7×10, 14/15 = 14×5).

Замер: уровень 4, комната 18, Кид сдвигается читом ] по пикселю (skip выключен, идёт полный путь), в fore-окне ШЕСТЬ тайлов, ТРИ из них — дворцовая стена.

участок тактов
пролог pop_char_fore до первого fore_tile 17 208
обычный тайл 4 000 4 600
тайл стены (было 60 000 74 000) ~12 700
хвост + чистка бортов 6 055
fore-проход целиком (было 171 693) 70 681

Кадр целиком в этой сцене: работа 419 839, период 3 растровых кадра (было 517 609 и 4).

Остаток в проходе — пролог 17 208, это уже пункт 1 ниже (футпринт из физики). Отдельная находка: когда Кид стоит НА ШВЕ (окно x 8..5), пролог разбухает до 134 730 — 86 % прохода; причина не разобрана, см. пункт 1.


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).