Профиль каскада плит ур.13 к.23: три рабочих документа по фазам

Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок).  Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.

Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх.  По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.

Что нашлось (всё подтверждено зондами, не гипотезы):

  RDA_CEIL      дрожащая плита-потолок     48 658 x до 6 = 292 000
  RDA_CEIL_GONE запечь колодец            251 023 x до 2 = 619 000
  RD_FLOOR      щебень на месте посадки   198 259 x до 2 = 397 000

Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600.  У blit_b_clip — 22 Б кадра и 211 (ix).

Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.

Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.

Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60.  Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).

Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):

  docs/perf_l13_room23.md  сцена, рецепт воспроизведения, зонды, канал clog,
                           сводка по кадрам, габариты спрайтов
  docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
  docs/perf_cyan_phase.md  циан: раскладка, позиции C1..C7, журнал

Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest).  Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 12:22:21 +03:00
parent c312e4a043
commit babd40bc84
5 changed files with 612 additions and 2 deletions
+4 -1
View File
@@ -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 |
+195
View File
@@ -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` |
+219
View File
@@ -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, колонки col1..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 00020 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`), где сейчас идёт пометка
каждый кадр отсчёта.
Вопрос открытый: стоит ли неочевидный код такого узкого выигрыша. Браться
после G1G4.
### 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` |
+168
View File
@@ -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 <addr>,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** | **56** |
| максимум по секции | | 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`.
+26 -1
View File
@@ -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.