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

154 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Оптимизация отрисовки — что НЕ сделано (замеры на 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_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`).