Три шага пункта 0 из docs/perf_backlog.md. 1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает — возврат до первой заливки. Замер: от входа в узор до конца всего прохода 6 055 тактов вместо ~60 000 на тайл. 2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту. pop_blit_b тоже отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски кладки высотой 3..12 px он пропускал и платил полный atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить, что рисовать нечего. Размеры сняты из каталогов атласов и заданы верхней границей по группе. 3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих разделителях и обеих марках. Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip выключен, шесть тайлов в fore-окне, три из них — дворцовая стена): тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо 171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4. Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на месте. Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а). Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана. BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки (Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac). Меч в инвентаре не теряется. Это болезнь чита: в оригинале телепорта между комнатами нет и в режим боя без соперника попасть нечем. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 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) — меньше трети.
Профиль дворца (уровень 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 |
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 и только там
обнаруживает, что рисовать нечего.
Что делать (по возрастанию объёма):
- Ранний выход из
wall_pattern_palace: самый верхний пиксель узора —dmy - 59, самый нижний —dby + высота нижнего декаля. Одинif (pop_t_fclip_on && (fclip_y1 <= dmy - 59 || fclip_y0 > dby + h)) return;плюс такая же проверка поxубивает оба тайла целиком почти даром. - Прогнать
pop_wall_bв этом узоре через ту же проверку окна, что уже есть уwpp_fill(нужны размеры кусков — см. п. 2 ниже, «размеры из каталога атласа»). - Сузить сам предфильтр
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).
Что сделано и сколько дало
- Ранний выход из
wall_pattern_palaceпо окну fore-клипа (узор целиком вx [xh*8, xh*8+32),y [dmy−59, dby]). - Отсев каждого декаля (
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. - То же для ПОДЗЕМЕЛЬЯ: ранний выход
wall_pattern(габарит там выше — левая марка уходит наdby+POP_YOFF−67) плюс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_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).