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

336 lines
24 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}`. Если
адрес возврата в банке (>= 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_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`).