diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index 028415e..3809363 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -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, остальное впереди | diff --git a/applications/PoP/docs/perf_backlog.md b/applications/PoP/docs/perf_backlog.md new file mode 100644 index 0000000..e8f338f --- /dev/null +++ b/applications/PoP/docs/perf_backlog.md @@ -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 ,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`). diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 033ebb3..46ef7d6 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -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` + расширение окна diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index 2eb9da3..e040dbb 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -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(); }