# Оптимизация отрисовки — что НЕ сделано (замеры на 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 ,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 %). ## 0. Дворцовая кладка в fore-проходе — 27 % кадра НА НОЛЬ ПИКСЕЛЕЙ **Цена: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей — ровно ноль.** Самая дорогая известная позиция; снятие её одной уводит работу с 517 609 до ~378 000, то есть период с **4 растровых кадров на 3**. Замер (уровень 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. Футпринт персонажа — брать из физики, а не считать заново **Цена: 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`).