diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index c6d92ba..af59f08 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -9,7 +9,10 @@ | [`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) | | [`../roomtest/BUGS_OPEN.md`](../roomtest/BUGS_OPEN.md) | Открытые баги roomtest (закрытые — в `BUGS_CLOSED.md` рядом) | | [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему | -| [`perf_backlog.md`](perf_backlog.md) | **Отложенная оптимизация отрисовки** с замерами + как мерить (wait-state'ы, границы кадра) | +| [`perf_l13_room23.md`](perf_l13_room23.md) | **Сцена и метод замера кадра** (каскад плит, ур.13 к.23): как воспроизвести, зонды, канал `clog`, сводка по кадрам, габариты спрайтов и ответ про `uint8_t`. 2026-08-17 | +| [`perf_green_phase.md`](perf_green_phase.md) | **ЗЕЛЁНАЯ фаза (слой фона)**: раскладка тактов, способы ускорения (G1..G6), журнал правок — рабочий документ между сессиями. 2026-08-17 | +| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 | +| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 | | [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 | diff --git a/applications/PoP/docs/perf_cyan_phase.md b/applications/PoP/docs/perf_cyan_phase.md new file mode 100644 index 0000000..cc301f3 --- /dev/null +++ b/applications/PoP/docs/perf_cyan_phase.md @@ -0,0 +1,195 @@ +# ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация + +Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая +запись с замером до/после. Сцена, рецепт воспроизведения и зонды — +[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные +габариты. Парная фаза — [`perf_green_phase.md`](perf_green_phase.md). + +Границы фазы в `roomtest.c`: от `PROF(6)` (строка 620) до `PROF(0)` +(строка 677). Содержимое: `pop_check_mirror`, `pop_loose_mob_draw`, +соперник, `pop_char_draw(KID)`, `pop_loose_mob_draw_over`, `pop_fore_needed`, +`pop_hp_draw`, `pop_char_fore(KID)`, `pop_cd_clear`, +`pop_room_clip_borders`. + +**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** + +| состояние | циан | +|---|---:| +| покой, Кид пропущен (`pop_char_skip_mask`) | 20 550 … 28 700 | +| Кид перерисовывается, кусков нет | 107 000 … 193 000 | +| **пик каскада (6 кусков в воздухе + Кид)** | **632 000 (1,6× бюджета)** | + +--- + +## 1. Раскладка (замер 2026-08-17, `c312e4a`) + +### 1.1 Подфазы (три готовых `PROF(6)`: 0x4BCE / 0x4C26 / 0x4C93) + +| участок | покой | пик (кадр 20) | максимум | +|---|---:|---:|---:| +| `pop_check_mirror` + `pop_loose_mob_draw` (куски ПОД Кидом) + соперник | 23 250 | 34 950 | **154 512** | +| `pop_char_draw(KID)` + `pop_loose_mob_draw_over` + `pop_fore_needed` + HP | 3 726 | **445 284** | 445 284 | +| `pop_char_fore(KID)` + `pop_cd_clear` + чистка бортов | 1 722 | **150 978** | 150 978 | + +### 1.2 Блиты кусков + +Блитов в циане — **19 на кадр**: шесть кусков × три части спрайта +(`env 74` = 32×3, `env 70` = 32×13, `env 72` = 26×16, порт `draw_mob` +seg007:13E5 / индексы `loose_fram_*[10]`). + +| | такты | +|---|---:| +| Σ 19 блитов | **313 758** | +| на вызов | **16 273** | + +То есть **половина фазы — это девятнадцать вызовов `pop_blit_b`.** + +### 1.3 Внутри одного `pop_blit_b` (157 замеров быстрого пути, зонды b1..b5) + +| участок | такты | доля | +|---|---:|---:| +| `atlas_image` + `gfx_w0_map` + чтение габарита | 1 086 | 7 % | +| ядро блита (libbgi, `gfx_blit_noclip`) | 10 422 | 64 % | +| **`pop_cd_touch` — пометка «фон тронут»** | **4 502** | **28 %** | +| `gfx_w0_unmap` + возврат | 264 | 2 % | +| ИТОГО | 16 273 | | + +Для сравнения, клипованный путь (227 замеров, полоса у потолка): ядро +`blit_b_clip` **14 088**, итого на вызов **16 409**. + +По модели `blit_cost_model` «железный» минимум передачи для этих трёх +спрайтов — 5 000 … 6 200 тактов. Остальные ~10 000 на вызов — накладные. + +### 1.4 `pop_char_fore(KID)` вырастает в каскаде на два порядка + +В покое 1 722, в кадрах каскада **150 978**. Кид стоит у правого края и НЕ +двигается, плиты падают в колонках 2..7 — то есть либо пропуск персонажа +(`pop_char_skip_mask`) гасится пометками `pop_cd_touch` от кусков, либо окно +fore-клипа расширяется и в него попадают лишние тайлы. **Не разобрано** — +см. C4. + +### 1.5 Кодоген + +| функция | стековый кадр | обращений `(ix)` | ожидаемо тактов | +|---|---:|---:|---:| +| `blit_b_clip` (резидент) | **22 Б** | **211** | ~9 700 | +| `pop_blit_b` (резидент) | 6 Б | 78 | ~3 600 | +| `pop_cd_touch` (резидент) | — | 30 | ~1 380 | +| `mob_draw_pass` (bank 7) | 18 Б | 20 | ~900 | +| `mob_render` (bank 7) | — | 1 | — | + +Одно `-N(ix)` ≈ 19 номинальных тактов ≈ 46 замеренных +(memory `sprinter_wait_states_2x`). + +--- + +## 2. Способы ускорения + +По убыванию отдачи. + +### C1. Убрать избыточный `pop_cd_touch` в `mob_render` + +**Отдача: −81 000 (13 % фазы). Самая дешёвая правка из всех.** + +`pop_loose_mob_tick` уже помечает **весь коридор куска** одним вызовом +`pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64)` — с запасом на +путь, пройденный за кадр. А потом три блита внутри `mob_render` помечают +подмножества этого же прямоугольника, по 4 502 такта каждый: 3 × 4 502 × 6 +кусков = **81 000 тактов впустую**. + +Механизм уже есть: `pop_cd_batch_begin()` / `pop_cd_batch_end()` (в +`pop_tile.c`, применяется в `draw_tile`) — в пакете `pop_cd_touch` только +копит общий прямоугольник четырьмя сравнениями. Обернуть `mob_render` в +скобку. + +Осторожно: пометка обязана покрывать **весь коридор heal'а**, а не габарит +спрайта — иначе персонаж, попавший в расширенную часть коридора, стирается, +но перерисовать себя не просит и исчезает с экрана (ровно этот баг ловили +2026-08-13: в комнате 23 после падения гряды пропадал Кид). Поскольку +коридорную пометку из тика мы НЕ трогаем, инвариант сохраняется. + +### C2. `pop_cd_touch` — байтовые аргументы + +**Отдача: часть от 4 502 на вызов; после C1 в циане останется 6 вызовов +(по одному на кусок), но в зелёной фазе и в fore-проходе их больше.** + +Четыре `int`-аргумента на вызов, 30 `(ix)` внутри. `w`/`h` заведомо ≤ 255 +(габарит спрайта ≤ 56×63), `x`/`y` — экранные, 16-битные. Это позиция (а) +задачи OPT-BLIT. + +### C3. `blit_b_clip` — 22 Б кадра, 211 `(ix)` + +**Отдача: 14 088 → ~10 500 на клипованный блит.** В циане клипованный путь +берут спрайты у краёв поля и весь fore-проход; в зелёной фазе — все блиты +полосы у потолка. Подробнее — [`perf_green_phase.md`](perf_green_phase.md) §G3. + +`sx/sy/dw/dh` → `uint8_t`, `dx/dy` оставить 16-битными. Прецедент: тот же +приём в `pop_blit_b` (коммит `c312e4a`) дал −19 % на клипованном пути. + +### C4. Разобрать `pop_char_fore(KID)`: 1 722 → 150 978 + +**Отдача неизвестна, но это 24 % фазы — мерить обязательно.** + +Гипотезы (проверять зондами, не рассуждением — memory +`defer_unexplained_quirks`): + +1. пометки `pop_cd_touch` от кусков гасят `pop_char_skip_mask` для Кида, хотя + куски падают в других колонках — проверить `pop_cd_dmask` в кадре; +2. `mob_mark_neighbour` ставит `pop_set_redraw_fore` на 1–2 тайла на каждый + кусок каждый кадр (порт `draw_mob`, seg007:1147), и `pop_fore_needed` + перерисовывает передние части 6–12 тайлов; +3. окно fore-клипа Кида расширено под клинок и брызги, и в него попадает + больше тайлов, чем нужно — известный пункт 1 в `perf_backlog.md` + («футпринт персонажа — брать из физики»): оригинал расширяет футпринт + только на одну колонку при вынутом мече и объединяет с футпринтом прошлого + кадра, а мы расширяем окном клипа. + +Зондов на подфазы циана больше нет — понадобятся временные `pop_dbg_*` +вокруг `pop_fore_needed` и `pop_char_draw`, то есть пересборка (после неё +ВСЕ адреса зондов меняются, брать заново из `roomtest.map`). + +### C5. Один `gfx_w0_map`/`unmap` на группу блитов + +**Отдача: 1 086 + 264 = 1 350 на вызов; три части одного куска лежат в одной +странице атласа → −2 700 на кусок, −16 000 на кадр.** + +Позиция 3 старого `perf_backlog.md`. Нужна форма «открыть страницу, N +блитов, закрыть»; мешает то, что `pop_blit_b` — общий лист для всех +вызывающих. Для `mob_render` (три блита из одного атласа `pop_env`) частный +случай тривиален. + +### C6. Размеры ленты — из каталога атласа + +**Отдача: часть от 1 086 на вызов.** Позиция 2 старого `perf_backlog.md`: +`fw`/`fh` уже лежат в записи каталога (8 байт: `offset u16, fw u8, fh u8, +nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = ny = 1`, то есть они +в точности равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает. + +### C7. objtable вместо отдельного fore-прохода на персонажа + +Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг, браться только +если после C1–C5 бюджет всё ещё не выполняется. Оригинал кладёт персонажей и +куски в `objtable` и рисует их при обходе тайлов +(`draw_objtable_items_at_tile`), а порядок окклюзии получается сам; у нас +отдельный fore-проход НА КАЖДОГО персонажа. + +--- + +## 3. Что НЕ делать + +- **Не ставить W3-скобку из кода с `--w3`** — белый экран + (memory `gfx_blit_noclip_fast`). +- **Не ускорять передачу пикселей** — предел железа + (memory `blit_cost_model`). +- **Не возвращать клип куска по `clip.right = 40`** оригинала + (`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены, + буквальные 40 экранных пикселей срезают правый задний угол плиты + (прогон 2026-08-13). + +--- + +## 4. Журнал правок + +| дата | что сделано | циан: покой / Кид / пик | коммит | +|---|---|---|---| +| 2026-08-17 | базовый замер (до правок этой серии) | 20 550 / 193 000 / **632 000** | `c312e4a` | diff --git a/applications/PoP/docs/perf_green_phase.md b/applications/PoP/docs/perf_green_phase.md new file mode 100644 index 0000000..ccd186c --- /dev/null +++ b/applications/PoP/docs/perf_green_phase.md @@ -0,0 +1,219 @@ +# ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация + +Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая +запись с замером до/после. Сцена, рецепт воспроизведения и зонды — +[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные +габариты. Парная фаза — [`perf_cyan_phase.md`](perf_cyan_phase.md). + +Границы фазы в `roomtest.c`: от `PROF(4)` (строка 450) до `PROF(6)` +(строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`, +`pop_redraw_needed`, шов, смена уровня, сигналы провалов, вспышка. + +**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).** + +| состояние | зелёная | +|---|---:| +| покой в комнате 23 | 35 760 | +| дрожат 6 плит-потолков | 366 000 | +| **пик каскада (провалы + посадки)** | **805 000 (2,0× бюджета)** | + +--- + +## 1. Раскладка (замер 2026-08-17, `c312e4a`, зонды m5/m6/m7 + kind/m16) + +### 1.1 Верхний уровень + +| участок | покой | дрожь | пик | +|---|---:|---:|---:| +| `pop_loose_tick` | 25 602 | 35 646 | **196 152** | +| `pop_process_trobs` | 1 050 | 1 050 | 1 050 | +| **`pop_redraw_needed`** | 924 | **319 944** | **737 322** | +| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 11 964 | + +**`pop_redraw_needed` — 91 % фазы на пике.** + +### 1.2 `pop_loose_tick` изнутри (зонды m9..m12) + +| участок | покой | пик | +|---|---:|---:| +| два цикла по тайлам (30 + 10 позиций) | 11 004 | 9 852 | +| **`pop_loose_mob_tick`** (heal летящих кусков) | 7 416 | **178 494** | +| `check_loose_fall_on_kid` | 6 120 | 3 480 | +| хвост | 324 | 3 252 | + +`pop_loose_mob_tick` при шести кусках в воздухе — **≈29 700 такта на кусок**. +Это heal прошлой позиции (коридор `MOB_W × 64`) плюс `pop_cd_touch` на +коридор плюс `mob_tick_one`. + +### 1.3 Цена ОДНОЙ перерисовки по видам (66 + 12 + 12 замеров) + +| вид | штук замерено | средняя цена | сколько за кадр | Σ за кадр | +|---|---:|---:|---:|---:| +| `RDA_CEIL` — дрожащая плита-потолок | 66 | **48 658** | до 6 | 292 000 | +| `RDA_CEIL_GONE` — запечь колодец | 12 | **251 023** | до 2 | 619 000 | +| `RD_FLOOR` — щебень на месте посадки | 12 | **198 259** | до 2 | 397 000 | + +Для масштаба: ОДНА запечка колодца = 58 % растрового кадра, две = 144 %. +`pop_dbg_rdmax` подтверждает состав пика: шесть пометок, все `RDA_CEIL`. + +### 1.4 `RDA_CEIL` = 48 658 изнутри (зонды m13/m14/m15) + +| участок | такты | +|---|---:| +| `pop_heal_off(x, 0, 64, CEIL_BAND_H=8)` | 8 491 | +| `draw_tile(-1, col)` | **39 029** | + +Блитов внутри этого `draw_tile` — **1,17 на тайл** (7 вызовов `pop_blit_b` +на 6 тайлов), то есть 15–16 тыс. тактов. +**Остальные ~23 700 такта — накладные `draw_tile`, ни одного пикселя.** + +### 1.5 `RDA_CEIL_GONE` = 251 023 и `RD_FLOOR` = 198 259 + +- `pop_ceil_bake_empty(col)` — бар + **шесть** `draw_tile` + (ряд −1 и ряд 0, колонки col−1..col+1): 6 × 39 000 ≈ 234 000. +- `pop_floor_bake(row,col)` — бар 60×39 от `yb+26` + **два** `draw_tile` + НИЖНЕГО ряда; там стена, то есть `wall_pattern` — ≈**99 000 на тайл**. + +### 1.6 Почему `draw_tile` стоит 39 000 при одном блите + +Кодоген. Подсчёт в `bank7_pop_room.asm` и совпадение с замером до процентов +(одно `-N(ix)` ≈ 19 номинальных тактов ≈ 46 замеренных): + +| функция | стековый кадр | обращений `(ix)` | ожидаемо | замерено | +|---|---:|---:|---:|---:| +| `draw_tile` | **26 Б** | **513** | ~23 600 | **~23 700** | +| `pop_ceil_bake_empty` | — | 8 | ~370 | | +| `pop_floor_bake` | — | 12 | ~550 | | +| `mob_tick_one` | 16 Б | 99 | ~4 550 | из ~29 700/кусок | +| `mob_draw_pass` | 18 Б | 20 | ~900 | | + +У `draw_tile` пятнадцать локалей, объявленных `int` +(`code`/`lcode`/`lmod`/`xh`/`x`/`dby`/`dmy`/`t`/`lt`/`rbl_code`/`rbl_mod`/ +`wmod`/`base_id`/`base_yb`), — в регистры они не влезли, и функция целиком +уехала в стековый кадр. Байтовые по природе из них все, кроме `x`/`dby`/`dmy` +(экранные координаты, бывают отрицательными). + +--- + +## 2. Способы ускорения + +По убыванию отдачи. Оценки — от замеров выше; каждая позиция сверена с +`SDLPoP/src/seg008.c` (правило проекта: механику сверять с оригиналом ДО +кодинга). + +### G1. Окно клипа для ЗАПЕЧКИ — самый крупный кусок + +**Отдача: −250 000 … −400 000 на пиковый кадр.** + +`pop_ceil_bake_empty` и `pop_floor_bake` восстанавливают ИЗВЕСТНЫЙ +прямоугольник (тот, который сами же залили баром), а зовут полный +`draw_tile`, который честно рисует и то, что в этот прямоугольник не попадает. +У `pop_floor_bake` бар — 60×39 от `yb+26`, а тело дворцовой кладки рисуется +НИЖЕ `yb+64`, то есть **вся стена рисуется мимо цели**. + +В `pop_blit_b` уже есть дешёвый предфильтр по окну `pop_t_fclip_*` — он режет +кусок ДО `atlas_image` и `gfx_w0_map`, то есть почти даром. Достаточно +выставлять это окно на время запечки. + +Грабли, которые надо обойти: `pop_t_fclip_on` в `pop_blit_b` заодно подавляет +`pop_cd_touch` (признак «это fore-проход поверх персонажа»). Для запечки это +как раз правильно — она помечает область сама, — но проверить обязательно; +если понадобится, ввести отдельный флаг «окно только для отсева». + +Сверка с оригиналом: у него та же идея, только через `wipetable` — +`draw_tile_wipe(height)` кладёт прямоугольник в таблицу, а `draw_table` +рисует уже с известным клипом (seg008:1368, 1373). + +### G2. Расколоть `draw_tile` + отдельный лист для ряда −1 + +**Отдача: 39 029 → 18 000–20 000 на тайл полосы, то есть −140 000 на кадр +дрожания; и она умножается на G1 (шесть вызовов в запечке).** + +Два шага: + +1. **Отдельный `draw_tile_aboveroom(col)`** — дословный порт seg008:01F2. + Оригинал для ряда −1 делает РОВНО шесть вещей: `draw_tile_floorright`, + `draw_tile_anim_topright`, `draw_tile_right`, `draw_tile_bottom(1)`, + `draw_loose(1)`, `draw_tile_fore`. Ни базы, ни `draw_tile_anim`, ни + `draw_tile_anim_right`, ни `wall_pattern`, ни вычисления `rbl_*` через + `row+1 > 2`. У нас всё это отсекается ветками `if (row >= 0)` уже ВНУТРИ + общего `draw_tile` — то есть кадр IX, пролог и вся арифметика оплачиваются + всегда. +2. **Расколоть общий `draw_tile` по образцу оригинала** (seg008:01C7 — + девять узких листьев). Каждый лист получает `xh`/`dby`/`dmy` параметрами + (в регистрах), локалей у него единицы, кадра IX нет. + +Заодно проверить `redraw_needed_above` (seg008:02C1): оригинал делает +`draw_tile_wipe(3)` — стирает **32×3**, а мы `pop_heal_off(x,0,64,8)` = +64×8. Площадь в 5 раз больше нужной (8 491 такта), хотя основная цена там — +накладные вызова. + +### G3. `blit_b_clip` — 22 Б кадра, 211 `(ix)` + +**Отдача: 14 088 → ~10 500 на блит. В зелёной фазе ВСЕ блиты клипованные +(полоса у потолка выставляет `pop_t_clip_top`) — при 7–17 блитах это +−25 000 … −60 000; в циане и fore-проходе больше.** + +`dx/dy/dw/dh/sx/sy` объявлены `int`. По построению `sx/sy/dw/dh ≤ 255` +(габарит любого спрайта ≤ 56×63, см. `perf_l13_room23.md` §5) — их можно +сделать `uint8_t`, оставив 16-битными только экранные `dx/dy`. +Прецедент: тот же приём в `pop_blit_b` (коммит `c312e4a`) дал −19 % на +клипованном пути и −16 % на быстром, `_CODE` −42 Б. + +Проверка по правилу OPT-BLIT: после правки смотреть пролог в +`.sprinter-cc-roomtest/pop_tile.asm` — исчез ли `ld iy,#-22`, сколько +осталось `-N(ix)`. + +### G4. `mob_tick_one` / `pop_loose_mob_tick` — 29 700 на кусок + +**Отдача: −30 000 … −48 000 на кадр при шести кусках.** + +16-байтовый кадр, 99 `(ix)`. Байтовыми могут быть габарит heal-коридора и +разность `prev_y`; `x`/`y` — 16-битные. Плюс `pop_cd_touch` на коридор — один +на кусок, 4 502 такта (см. `perf_cyan_phase.md` §1.3 про его цену). + +### G5. `LOOSE-SHAKE-RUNS` — пометки по сменам кадра + +**Отдача: ~20 % работы фазы, и только на дрожащих плитах. Оценка старая +(2026-08-13), не перемерена.** + +Подробности и арифметика — в `roomtest/TASKS_OPEN.md#loose-shake-runs`. +Коротко: наивный вариант «помечать только на смене кадра» выигрыша НЕ даёт +(пять смен × две страницы = те же десять перерисовок); работает только версия +«пары и тройки» — две пометки с `pages = 2` на фазах 5 и 8. Тот же приём +применим к плитам-ПОТОЛКАМ (`pop_set_redraw_above`), где сейчас идёт пометка +каждый кадр отсчёта. + +Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться +после G1–G4. + +### G6. `BG-ONCE` — heal вместо чёрного бара + +Предложение пользователя, см. `roomtest/TASKS_OPEN.md#bg-once`. Сейчас +запечка = «залить чёрным + перерисовать фон», то есть двойная работа; heal по +координатам бара делает то же за один проход. Прямо дополняет G1. + +--- + +## 3. Что НЕ делать (проверено, отрицательный результат) + +- **Не ускорять передачу пикселей** — она на пределе железа (3+3 такта на + байт через акселератор), memory `blit_cost_model`. В пиковом кадре + «железный» минимум всех блитов ≈150 000 из 1 437 150. +- **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из + которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры + (проверено 2026-08-13). +- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — пробовалось и + убрано: плиты стартуют со случайными задержками, в кадре дрожат разрозненные + колонки, пробег почти всегда длиной в одну. +- **Не искать проблему в W3-скобке** (`_bgi_begin`/`_bgi_end` — пять + инструкций) и не списывать разброс на прерывания (внутри размерной группы + разброс три такта). + +--- + +## 4. Журнал правок + +| дата | что сделано | зелёная: покой / дрожь / пик | коммит | +|---|---|---|---| +| 2026-08-17 | базовый замер (до правок этой серии) | 35 760 / 366 000 / **805 000** | `c312e4a` | diff --git a/applications/PoP/docs/perf_l13_room23.md b/applications/PoP/docs/perf_l13_room23.md new file mode 100644 index 0000000..a170307 --- /dev/null +++ b/applications/PoP/docs/perf_l13_room23.md @@ -0,0 +1,168 @@ +# Сцена и метод замера: каскад плит, уровень 13 комната 23 + +Общий документ для двух фазовых: [`perf_green_phase.md`](perf_green_phase.md) +(слой фона) и [`perf_cyan_phase.md`](perf_cyan_phase.md) (персонажи + передний +слой). Здесь — как воспроизвести сцену, чем мерить, сводка по кадрам и +разбор габаритов спрайтов (он общий для обеих фаз). + +Сцена: старт уровня 13. Комната 23 стартовая, ряд 2 комнаты СВЕРХУ (17) — +шесть loose-плит в колонках 2..7 (`res2013.bin`: коды `11` в позициях 22..27), +`check_fall_flo` раздаёт им отложенный старт `0xF0..0xFF`, и они сыплются +вразнобой. Кид стоит у правого края и не двигается. + +Все числа — такты `totalcycles` MAME (системный клок ~21,5 МГц, **НЕ** такты +Z80: у ОЗУ Sprinter wait-state'ы, ≈2,4× номинала — memory +`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Логический кадр +спейсится тремя `gfx_wait_vsync`, поэтому работа сверх 430 000 стоит сразу +целый лишний растровый кадр. + +Сборка: `make LEVEL=13` на `c312e4a`, `_CODE = 0x4100`, база модуля +`roomtest.c` = **0x42AD**. **Адреса зондов меняются после КАЖДОЙ +пересборки** — брать заново из `.sprinter-cc-roomtest/roomtest.map` и +`roomtest.lst`. + +--- + +## 1. Как воспроизвести сцену + +**Только перезапуском программы.** Проверено и отвергнуто: + +- **выход из комнаты и возврат** (чит `+`/`-`) — не работает: провалившаяся + плита-потолок уходит в страницу уровня насовсем (`pop_level_set_tile` в + `roomtest.c` по сигналу `pop_ceil_fell`, плюс `animate_loose` в + `pop_trob.c`), и при повторном входе `check_fall_flo` не находит ни одной + `TILE_LOOSE`; +- **рестарт уровня** (`pop_kid_dead = 1` + чит навигации) — не работает по + другой причине: `pop_start_level()` сам заходит в стартовую комнату 23, + взводит гряду, и она доваливается ЗАОЧНО (через `trob` комнаты 17), пока + телепорт уносит Кида в комнату 24; +- **поставить сцену руками** (записать `pop_ceil_modif[2..7]` и копию ряда + сверху `pop_t_above[2..7]` отладчиком) — записи ложатся, но пока машина + БЕЖИТ, их успевает обнулить тот же доваливающийся `trob`. + +Рабочий рецепт (идея пользователя, самый чистый): **`ESC` → зонды → +`roomtest`**. `ESC` выходит в DSS, запуск заново стартует уровень 13 с нуля, +Кид сразу в комнате 23, каскад начинается через ~5 логических кадров после +отрисовки комнаты. Зонды обязаны стоять **ДО** набора `roomtest` — за время +набора (9 клавиш ≈ 1,8 с) и загрузки атласов каскад успевает пройти целиком. + +## 2. Канал вывода замеров + +`printf` из действия брейкпоинта в `error.log` **не** попадает. Читается +verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP-обёртке +(`mame_mcp.py` знает только `cmd`). Годится прямой файловый IPC: +положить `/tmp/mame_mcp/req_<ЧИСЛО>.txt` с телом команды и прочитать +`resp_<ЧИСЛО>.txt`. **Имя обязано содержать ЧИСЛО** (`init.lua`: +`entry:match("^req_(%d+)%.txt$")`) — с буквенным id запрос молча не +обслуживается. + +Скрипты сессии (в scratchpad, при необходимости пересоздать): `mrpc.py` — +клиент IPC; `run.sh` — цикл «`bpclear` → `ESC` → зонды → `roomtest` → `clog`»; +`parse*.py` — разбор трассы по кадрам. + +Форма зонда: `bpset ,1,{printf "<метка> %d",totalcycles; g}`. +Для `pop_dbg_kind` — `printf "K %d %d",a,totalcycles` (аргумент `uint8_t` +приходит в `A`, `__sdcccall(1)`). + +## 3. Зонды + +Адреса `out (_io_border), a` (полосы бордюра) из `roomtest.lst` плюс +однобайтовые пустышки `pop_dbg_*` из резидентного `pop_state.c`. Резидент +важен принципиально: у банковых функций один адрес 0xC000+ есть у восьми +модулей сразу, и брейкпоинт ловит все банки (так в прошлой сессии намерили +несуществующие 134 730 тактов). + +| зонд | адрес | что | +|---|---|---| +| A | 0x43E8 | `PROF(2)` — начало кадра (ввод + heal) | +| — | 0x46AF | `PROF(2)` — начало логики | +| C | 0x47D1 | `PROF(4)` — начало слоя фона (**зелёная**) | +| D | 0x4BCE | `PROF(6)` — начало спрайтов (**циан**) | +| M | 0x4C26 | `PROF(6)` — кадр Кида | +| F | 0x4C93 | `PROF(6)` — fore поверх Кида | +| E | 0x4CB7 | `PROF(0)` — конец работы, ждём vsync | +| m5/m6/m7 | 0x4DC6 / C7 / C8 | границы внутри зелёной | +| m9..m12 | 0x4DCA..CD | внутренности `pop_loose_tick` | +| m13/m14/m15 | 0x4DCE / CF / D0 | `pop_ceil_shake_draw`: вход / heal / draw_tile | +| kind / m16 | 0x4DD1 / D2 | вид и цена одной перерисовки в `pop_redraw_needed` | +| b1..b5 | 0x4DD3..D7 | участки одного `pop_blit_b` | + +Полезные адреса состояния (из `roomtest.map`): `pop_t_room` 0x95F2, +`pop_current_level` 0x9945, `pop_ceil_modif` 0x9C9E, `pop_t_above` 0x95EE +(указатель), `pop_kid_dead` 0x9C4E, `pop_dbg_rdmax` 0x95B1. +Запись в память через MCP — по адресу `0x10000 | addr` (логический вид Z80); +присваивание выражением дебаггера (`print b@... = 1`) **не работает**. + +**Грабли:** проверять, что запущен РОВНО ОДИН MAME (`pgrep -f mame.arm | wc -l`). +Мост говорит с одним, замеры собираются с другого, и точки «не срабатывают». + +--- + +## 4. Сводка по кадрам + +| фаза каскада | работа | синяя | зелёная | циан | период (растр.) | +|---|---:|---:|---:|---:|---:| +| покой в комнате 23 | 190 860 | 134 550 | 35 760 | 20 550 | **3** | +| дрожат 6 плит | 537 400 | 142 700 | 366 000 | 28 700 | **4** | +| провалы + полёт, ПИК | **1 437 150** | 142 700 | **663 250** | **631 200** | **5–6** | +| максимум по секции | | 142 830 | **805 000** | **631 800** | | + +Цель — каждая секция ≤ 400 000. **Синяя в норме**; зелёная 2,0× бюджета, +циан 1,6×. Логический кадр вместо 3 растровых занимает 5–6: в каскаде игра +идёт вдвое медленнее нормы. + +Где что расходуется и как это чинить — в фазовых документах: +[зелёная](perf_green_phase.md), [циан](perf_cyan_phase.md). + +### Общий вывод по пиковому кадру (1 437 150) + +| | такты | доля | +|---|---:|---:| +| блиты (все 27–28 вызовов `pop_blit_b`) | 488 100 | 34 % | +| из них «железный» минимум пикселей (модель `198*h + 5,96*w*h`) | ~150 000 | 10 % | +| синяя (ввод + heal + логика) | 142 700 | 10 % | +| **наши накладные: `draw_tile`, IX-кадры, диспетчер, пометки** | **~1 150 000** | **~80 %** | + +Узкое место — НЕ передача пикселей (она на пределе железа, 3+3 такта на байт, +memory `blit_cost_model`), а 16-битная арифметика в стековых кадрах. + +--- + +## 5. Габариты спрайтов: можно ли всё перевести на `uint8_t` + +Просканированы каталоги ВСЕХ `.atl` (109 файлов) и исходные PNG наборов +`TITLE`/`PV` — тех, что понадобятся для интро, финала и роликов между +уровнями. + +**Игровой кадр — весь укладывается в байт:** + +| набор | максимум | +|---|---| +| фон подземелья/дворца (`*_env*`, `*_wall`, `*_fore`, `pop_pot`) | **48 × 63** | +| Кид (`kid0..27`, `sword`) | **56 × 63** (kid3, idx 0) | +| страж / скелет / Джафар | **53 × 42** | +| спрайты комнаты принцессы (`PV.DAT`: персонажи, песочные часы, факел, звёзды) | **49 × 60** | + +**Больше 255 — только полноэкранные подложки титров и сюжетных экранов.** +Их восемь, и все рисуются ОДИН раз при показе экрана: + +| ресурс | размер | где (`data.h`, `full_image[]`) | +|---|---|---| +| `TITLE/res51` | 320 × 200 | `TITLE_MAIN`, xpos 0 ypos 0 | +| `TITLE/res41` | 320 × 200 | `STORY_FRAME`, xpos 0 ypos 0 | +| `PV/res951` | 320 × 200 | фон комнаты принцессы (`chtab_9_princessbed`) | +| `TITLE/res42..res45` | 272 / 267 / 264 / **256** × 134..142 | «presents», «Prince of Persia», «Mechner» | +| `TITLE/res54` | 272 × 65 | заголовок Hall of Fame | + +Высота нигде не превышает 200 — в байт лезет. По ширине не лезут ровно эти +восемь, и ни одна из них не участвует в игровом кадре. + +**Вывод: горячий путь можно переводить на 8-битные габариты целиком.** +Для подложек — решение пользователя (2026-08-17): работу с роликами вынести в +отдельный банк с версиями блита под большие спрайты либо звать libbgi напрямую +— клип и проверка выхода за экран им не нужны (рисуются в x = 0/24/48/96, +заведомо внутри 320×200). Ширина 320 всё равно потребует ДВУХ burst-скобок +акселератора на строку — как уже сделано в `pop_vflip`. + +Существующая страховка уже есть и остаётся: `pop_blit_b` уводит кадр с +`img[1] | img[3] != 0` на общий путь `blit_b_oversize`. diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 91a3146..8721dc4 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -928,6 +928,29 @@ HP/минуты, номера «особых» комнат и уровней ( ## OPT-BLIT — цена отрисовки: чистка кодогена (СЛЕДУЮЩАЯ СЕССИЯ) +> **2026-08-17: контрольный замер снят, задача разложена по фазам.** +> Работа теперь ведётся в трёх документах, которые живут между сессиями — +> туда же писать результаты каждой правки: +> +> - [`../docs/perf_l13_room23.md`](../docs/perf_l13_room23.md) — сцена +> (каскад 6 плит, ур. 13 комната 23), рецепт воспроизведения, зонды, канал +> `clog`, сводка по кадрам, разбор габаритов спрайтов; +> - [`../docs/perf_green_phase.md`](../docs/perf_green_phase.md) — ЗЕЛЁНАЯ +> фаза: пик **805 000** при бюджете 400 000; позиции G1..G6; +> - [`../docs/perf_cyan_phase.md`](../docs/perf_cyan_phase.md) — ЦИАН фаза: +> пик **632 000** при бюджете 400 000; позиции C1..C7. +> +> Порядок работ: **G1** (окно клипа для запечки, −250..400 тыс.) → **G2** +> (расколоть `draw_tile` + лист для ряда −1) → **C1** (убрать избыточный +> `pop_cd_touch` в `mob_render`, −81 тыс.) → **G3/C3** (`blit_b_clip` на +> байтовые габариты) → **C4** (разобрать `pop_char_fore(KID)`: 1 722 → 150 978). +> +> Синяя секция (**142 830**) в бюджет укладывается — не трогаем. +> Ответ на вопрос про `uint8_t`: горячий путь можно переводить целиком, +> максимум по всем игровым атласам **56×63**; шире 255 только восемь +> полноэкранных подложек титров/сюжета (320×200 и т. п.), и они рисуются раз +> на экран — им отдельный банк / прямой libbgi. + Открыто 2026-08-13 по итогам разбора зелёного блока. Контекст и все замеры — в memory `blit_cost_model` и `sdcc_z80_stack_locals_hot_loop`, протокол разбора — в коммитах `f89b7dd` / `b27b313` / этом. @@ -994,7 +1017,9 @@ grep -n "pop.*\n.*pop.*\n.*push" ... # чтение спилла - Пакетная пометка `pop_cd_touch` (`pop_cd_batch_begin/end`, скобка в `draw_tile`) — сделана, но выигрыш замером НЕ подтверждён: в захваченных кадрах скобка не срабатывала (блиты шли из холодной отрисовки комнаты). - Переснять на кадрах ЗАПЕКАНИЯ. + Переснять на кадрах ЗАПЕКАНИЯ. **2026-08-17: цена НЕпакетного вызова + замерена — 4 502 такта, 28 % всей цены блита; в `mob_render` он к тому же + избыточен (см. C1).** - Снять временную оснастку: `pop_dbg_m9..m16`, `pop_dbg_b1..b6`, `pop_dbg_wh`, `pop_dbg_kind`, `pop_dbg_rdmax*`, `mame/v306/run_bridge_log.sh`. - Цель по кадру не достигнута: зелёный пик был 792 012 при цели 400 000.