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>
This commit is contained in:
2026-08-10 11:05:42 +03:00
parent a9f4521ffd
commit c8fe0bd37a
4 changed files with 211 additions and 29 deletions
+1
View File
@@ -9,6 +9,7 @@
| [`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
| [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) |
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему |
| [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
+153
View File
@@ -0,0 +1,153 @@
# Оптимизация отрисовки — что НЕ сделано (замеры на 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`).
+9
View File
@@ -439,6 +439,15 @@ wait-state'ы, и замеренная стоимость выходит **≈ 2
Синяя полоса (ввод + логика) была ~60 % → стала ~29 %.
**Оптимизация закрыта по решению пользователя 2026-08-10.** Всё, что
осталось неcделанным, вынесено с замерами в
[`../docs/perf_backlog.md`](../docs/perf_backlog.md) — там же протокол «как
мерить», чтобы не наступать заново на wait-state'ы и на устаревшие адреса
символов. Что доделано после таблицы выше: `pop_cd_touch` развёрнут,
tp/10 у факелов таблицей, пустой слот соперника считается тихим, ранний
выход в fore-проходе (нулевой эффект, оставлен как порт), быстрый путь без
клипа в `pop_blit_b` (факел 41 778 -> 31 218 тактов).
**Что осталось (запас на будущее, срочности больше нет):**
1. **`pop_char_fore` 113 628.** Внутри: `char_footprint` + расширение окна
+48 -29
View File
@@ -186,11 +186,45 @@ void pop_cd_touch(int x, int y, int w, int h)
}
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
* и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр
* платили и блиты БЕЗ клипа (`ld -n(ix)` = 19 тактов номинала, у нас ~46).
* А блитов без клипа большинство: весь фон вне fore-прохода — факелы, зелья,
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h)
{
int dx = x, dy = top, dw = w, dh = h, sx = 0, sy = 0;
if (pop_t_clip_top && dy < pop_t_clip_top) { /* полоса у потолка */
sy = pop_t_clip_top - dy; dy = pop_t_clip_top; dh -= sy;
}
if (pop_t_fclip_on) { /* окно fore-слоя */
if (dx < pop_t_fclip_x0) { int d = pop_t_fclip_x0 - dx; sx += d; dx += d; dw -= d; }
if (dy < pop_t_fclip_y0) { int d = pop_t_fclip_y0 - dy; sy += d; dy += d; dh -= d; }
if (dx + dw > pop_t_fclip_x1) dw = pop_t_fclip_x1 - dx;
if (dy + dh > pop_t_fclip_y1) dh = pop_t_fclip_y1 - dy;
}
if (dw <= 0 || dh <= 0) return;
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == w && dh == h)
gfx_blit_noclip(dx, dy, img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
} else {
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана */
}
/* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж —
* пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход
* поверх персонажа» — окно клипа: его ставит только он. */
if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh);
}
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{
const uint8_t *img;
uint16_t w, h;
int top, dx, dy, dw, dh, sx = 0, sy = 0;
int top;
if (idx >= a->count)
return;
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
@@ -208,37 +242,22 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
dx = x; dy = top; dw = (int)w; dh = (int)h;
if (pop_t_clip_top && dy < pop_t_clip_top) { /* полоса у потолка */
sy = pop_t_clip_top - dy; dy = pop_t_clip_top; dh -= sy;
}
if (pop_t_fclip_on) { /* окно fore-слоя */
if (dx < pop_t_fclip_x0) { int d = pop_t_fclip_x0 - dx; sx += d; dx += d; dw -= d; }
if (dy < pop_t_fclip_y0) { int d = pop_t_fclip_y0 - dy; sy += d; dy += d; dh -= d; }
if (dx + dw > pop_t_fclip_x1) dw = pop_t_fclip_x1 - dx;
if (dy + dh > pop_t_fclip_y1) dh = pop_t_fclip_y1 - dy;
}
if (w && h && dw > 0 && dh > 0) {
/* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
* (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
* вызывать нельзя (окно W3 уходит под видеобанк). */
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
gfx_blit_noclip(dx, dy, img);
if (w && h) {
if (!pop_t_fclip_on && !pop_t_clip_top) {
/* БЫСТРЫЙ путь: клипа нет вовсе. Так идут все блиты фона вне
* fore-прохода. Линейное ядро без клипа — ~2.9× быстрее общего
* (замер: 4617 против 13288 тактов на спрайт 32×3; gfx_blit
* платит за клип/16-бит/split, а не за пиксели). W3-скобку
* ставит сама libbgi: из кода с --w3 её вызывать нельзя. */
if (x >= 0 && top >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && top + (int)h <= 256)
gfx_blit_noclip(x, top, img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
blit_b_clip(img, x, top, (int)w, (int)h);
}
/* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж —
* пометить область, чтобы его перерисовали (DRAW-COST, pop_cdraw.h).
* Признак «это не fore-проход поверх персонажа» — окно клипа: его
* ставит только он. */
if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh);
}
gfx_w0_unmap();
}