From fd570c7eb83a2db8a300008c6150f8f145cf5454 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 10:21:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=BC=D0=B5=D1=80=20=D1=81=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=2011/15=20=D0=B8=20=D0=B5=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=80=D0=B5=D0=B5=D1=81=D1=82=D1=80=20=D0=BE?= =?UTF-8?q?=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новая целевая сцена: уровень 11 комната 15 — два факела, чомпер, страж. В отличие от 13/23 (разовый пик на каскаде плит) здесь дорога САМА статика: Кид и страж стоят, а кадр стоит 801 768 тактов = 1,86 растрового кадра, период 4 растра во всех 866 интервалах прогона. Фазы: синяя 293 238 (heal 141 048 + логика 152 190), зелёная 320 916 (loose_tick 28 872 + process_trobs 92 448 + redraw_needed 190 260), циан 187 758. Впечатление «циан ~150 % кадра» не подтвердилось: за 867 кадров разброс циана 174 такта, это 0,44 растра. Главная находка — 190 260 тактов на ОДИН тайл (pop_dbg_rdmax_tot = 1). Пламя факела запекается в ячейке правого соседа, то есть поверх чомпера, и process_trobs метит соседа (порт set_redraw_anim_right). Оригинал на такую пометку рисует ТОЛЬКО слой anim, мы же отвечаем heal 32x64 плюс полный draw_tile — со всеми слоями, которых пламя не касалось. Заодно разложена цена одного блита фона (брейкпоинты на резидентных адресах внутри pop_blit_b, temp0 на входе, 1603 блита): фиксированная накладная 6 126 тактов на ЛЮБОЙ блит — пролог с IX-фреймом 810, atlas_image 672, w0_map с чтением шапки 2 400, cd_touch 2 069, unmap 175. У самого дешёвого блита это 59 % цены, у пламени 16x18 пиксели тянут лишь 12 %. Причины ровно те, на которые указал пользователь: 16-битные аргументы там, где хватает 8 бит, и адресация через IX. perf_registry.md сводит в один отсортированный список всё отложенное из perf_green_phase (G1-G9), perf_cyan_phase (C1-C7), perf_backlog (1-7), HEAL-WIDTH и сегодняшние находки — с пометкой замер/модель/гипотеза. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/perf_l11_room15.md | 108 ++++++++++++ applications/PoP/docs/perf_registry.md | 216 +++++++++++++++++++++++ applications/PoP/roomtest/TASKS_OPEN.md | 5 + 3 files changed, 329 insertions(+) create mode 100644 applications/PoP/docs/perf_l11_room15.md create mode 100644 applications/PoP/docs/perf_registry.md diff --git a/applications/PoP/docs/perf_l11_room15.md b/applications/PoP/docs/perf_l11_room15.md new file mode 100644 index 0000000..5b30360 --- /dev/null +++ b/applications/PoP/docs/perf_l11_room15.md @@ -0,0 +1,108 @@ +# Сцена и замер: факел + чомпер + страж, уровень 11 комната 15 + +Вторая целевая сцена для оптимизации (первая — [`perf_l13_room23.md`](perf_l13_room23.md), +каскад плит). Здесь узкое место другое: не разовый пик на каскаде, а +**постоянная** цена статичной комнаты, в которой одновременно живут два +факела, чомпер и страж. + +Такты — `totalcycles` MAME (не такты Z80, ≈2,4× номинала, memory +`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Хвост кадра — три +`gfx_wait_vsync`, поэтому логический кадр занимает 3 растра, пока работа +укладывается в один; при работе 1..2 растра период становится 4. + +## 1. Сцена + +Уровень 11, комната 15. Проверено чтением состояния машины: +`pop_current_level` = 0x0B, `cur_room` = 15. + +| кто | где | кадр | +|---|---|---| +| Кид | (0,2), x=98 | 15 (стойка с мечом) | +| чомпер | (0,3) | застывший (trob мёртв) | +| факел | (0,2) — пламя рисуется в ячейке (0,3) | анимируется каждый кадр | +| факел | (0,7) — пламя в ячейке (0,8) | анимируется каждый кадр | +| страж | (0,8), x=170 | 171 (боевая стойка) | + +Ни Кид, ни страж не двигаются: сцена статична, разброс замера — сотни тактов +на 800 000. + +## 2. Замер (`09f32ce`, база модуля `roomtest.c` = 0x42AD) + +867 кадров, зонды A/C/D/E; детализация — тремя отдельными прогонами +(m1, m5..m7, M/F). **Период кадра: 4 растра во всех 866 интервалах.** + +| участок | зонды | медиана | доля работы | +|---|---|---:|---:| +| **синяя: ввод + heal** | A→m1 | 141 048 | 17,6 % | +| **синяя: логика** | m1→C | 152 190 | 19,0 % | +| синяя, всего | A→C | **293 238** | 36,6 % | +| зелёная: `pop_loose_tick` | C→m5 | 28 872 | 3,6 % | +| **зелёная: `pop_process_trobs`** | m5→m6 | 92 448 | 11,5 % | +| **зелёная: `pop_redraw_needed`** | m6→m7 | 190 260 | 23,7 % | +| зелёная: шов/ворота соседа | m7→D | 9 336 | 1,2 % | +| зелёная, всего | C→D | **320 916** | 40,0 % | +| циан: `check_mirror` + `loose_mob_draw` | D→M | 37 458 | 4,7 % | +| **циан: Кид + страж + fore + HP** | M→F | 148 566 | 18,5 % | +| циан: `char_fore(KID)` + борта | F→E | 1 740 | 0,2 % | +| циан, всего | D→E | **187 758** | 23,4 % | +| **работа** | A→E | **801 768** | 1,86 растра | + +Циан здесь **не** выделяется: 187 758 — это 0,44 растрового кадра, а разброс +за 867 кадров всего 174 такта (187 674..187 848). Впечатление «циан ~150 % +кадра» на глаз не подтвердилось — при периоде 4 растра полосы бордюра +размазаны по кадрам и на глаз не читаются. + +## 3. Главная находка: чомпер справа от факела — 190 260 тактов/кадр + +`pop_dbg_rdmax_tot` = **1**: за кадр перерисовывается РОВНО ОДИН тайл, и +стоит он все 190 260 тактов зелёной фазы. + +Цепочка: + +1. `TORCH_ANIM_DIV = 1` — факел меняет кадр пламени КАЖДЫЙ логический кадр; +2. пламя запекается в фон (`pop_torch_draw`, `GFX_BANK_NORMAL`), а канвас + пламени 16×18 лежит **в ячейке правого соседа** (seg008:560) — то есть + поверх чомпера; +3. поэтому `pop_process_trobs` метит соседа: `if (trob_rcode[i] == TILE_CHOMP) + pop_set_redraw(tp + 1, POP_RD_CHOMP, 1)` (порт `set_redraw_anim_right`); +4. `pop_chomp_redraw` отвечает на пометку **heal 32×64 + полный `draw_tile`**. + +Расхождение с оригиналом именно в шаге 4. `set_redraw_anim_right` метит +слой **anim**, и оригинал возвращает только его (`draw_tile_anim_topright` → +`draw_tile_anim_right` → `draw_tile_anim`) — одну графику чомпера поверх +огня. Мы вместо этого стираем и пересобираем тайл целиком, со всеми слоями +(`draw_tile_right`, `base`, `bottom`, `loose`), которые пламя вообще не +трогало. + +Цена по модели блита (`blit_cost_model`, 8791 + 198·h + 5,96·w·h): +heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ 156 000 — сходится +с известным замером «полная запечка щебня 179 914». + +Чомпер при этом **застывший**: своей анимации у него нет, поза не меняется, +возвращать нужно ровно ту же графику поверх свежего пламени. + +## 4. Что это даёт и куда смотреть дальше + +Ранжирование по цене (доля от 801 768): + +| # | участок | такты | что делать | +|---|---|---:|---| +| 1 | `redraw_needed`: чомпер под факелом | 190 260 | вернуть только слой anim, как в оригинале — без heal и без остальных слоёв | +| 2 | синяя: логика двух Char | 152 190 | графики нет вообще; разобрать `pop_check_can_guard_see_kid` и два `play_seq` | +| 3 | циан: Кид + страж | 148 566 | оба будятся каждый кадр — метки фона от чомпера/факела накрывают обоих | +| 4 | синяя: heal двух Char | 141 048 | следствие того же: skip не срабатывает ни разу | +| 5 | `process_trobs`: два факела | 92 448 | ≈46 000 на факел при блите пламени 16×18 ≈ 14 000 — разобрать накладные | +| 6 | `loose_tick` | 28 872 | в комнате нет ни одной loose-плиты | + +Пункты 3 и 4 — одна тема: пока фон трогают каждый кадр, `pop_char_skip_mask` +не может пропустить ни Кида, ни стража. Пламя метит узко (16×18), а вот +`pop_chomp_redraw` метит весь тайл со свесом — то есть пункт 1 чинит и часть +пунктов 3/4. + +Связанные задачи: `HEAL-WIDTH` и G8 в [`perf_green_phase.md`](perf_green_phase.md) +— та же болезнь (полный тайл там, где хватает полосы), но на другом +материале. + +**Полная очередь оптимизаций с оценками — [`perf_registry.md`](perf_registry.md).** +Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603 +блита) и модель зелёной фазы этой сцены. diff --git a/applications/PoP/docs/perf_registry.md b/applications/PoP/docs/perf_registry.md new file mode 100644 index 0000000..0463249 --- /dev/null +++ b/applications/PoP/docs/perf_registry.md @@ -0,0 +1,216 @@ +# Реестр оптимизаций: всё отложенное, в одном списке + +Собрано 2026-08-19 из [`perf_green_phase.md`](perf_green_phase.md) (G1-G9), +[`perf_cyan_phase.md`](perf_cyan_phase.md) (C1-C7), +[`perf_backlog.md`](perf_backlog.md) (позиции 1-7), +[`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) (HEAL-WIDTH) и из +свежего разбора сцены [`perf_l11_room15.md`](perf_l11_room15.md). + +**База для процентов — работа кадра в 11/15: 801 768 тактов** (замер +`09f32ce`). Где эффект относится к другой сцене, это сказано явно. + +Оценки помечены: **[замер]** — измерено; **[модель]** — посчитано по +измеренным составляющим; **[гипотеза]** — не мерено, нужен прогон. + +--- + +## 1. Цена одного блита фона — разложена [замер 2026-08-19] + +Метод: брейкпоинты на резидентных адресах внутри `pop_blit_b` (0x5C50), +`temp0` на входе, разница `totalcycles` на каждом вызове. 1603 блита. + +| этап | такты | постоянство | +|---|---:|---| +| пролог + аргументы + грубый отсев | **810** | ровно, всегда | +| `atlas_image` | **672** | ровно, всегда | +| `gfx_w0_map` + чтение шапки ленты + арифметика клипа | **2 400** | ровно, всегда | +| ядро блита (пиксели) | 4 380 … 26 592 | по размеру кадра | +| `pop_cd_touch` | **2 069** (пакетный путь) / 4 115 (настоящая пометка) | почти ровно | +| `gfx_w0_unmap` + эпилог | **175** | ровно, всегда | +| **весь блит** | 10 458 … 32 718, медиана **16 674** | | + +**Фиксированная накладная = 6 126 тактов на любой блит, хоть 8×8.** +У самого дешёвого блита (10 458) это **59 % цены**; у пламени факела 16×18 +пиксели тянут ~1 700 из ~14 000, то есть **12 %**. + +Это и есть ответ на вопрос «почему маленький блит стоит 14 000». Причины +ровно те, о которых спрашивал пользователь: + +- **810 на пролог** — `call ___sdcc_enter_ix`, IX-фрейм и шесть чтений + `N(ix)`: третий и четвёртый аргументы (`int x`, `int ybottom`) идут + СТЕКОМ, каждое обращение 19 тактов Z80; +- **2 069 на `pop_cd_touch`** даже по пакетному пути, где вся работа — четыре + сравнения. Сигнатура `(int x, int y, int w, int h)` = 8 байт аргументов, + два из них через стек; `w`/`h` никогда не больше 64, `y` не больше 255, + то есть три из четырёх могли быть `uint8_t`; +- **2 400 на map + шапку** — `gfx_w0_map` (1 086) + `unmap` (264, платится в + конце) + ~1 000 на чтение четырёх байт заголовка и арифметику; +- **672 на `atlas_image`** — маппинг W3, чтение записи каталога, возврат W3; + размеры при этом читаются и выбрасываются (см. §3, C6). + +**Блитов за кадр в 11/15: 8** [замер] — все в зелёной фазе (6 на `draw_tile` +чомпера, 2 на пламя факелов). Синяя и циан через `pop_blit_b` не ходят +вовсе: heal и персонажи идут своими путями. Значит фиксированные накладные +блита стоят сцене **8 × 6 126 = 49 000 тактов/кадр (6,1 % работы)**. + +--- + +## 2. Модель зелёной фазы 11/15 [модель, сходится с 320 916 замера] + +| статья | такты | доля фазы | +|---|---:|---:| +| 8 блитов фона (из них 6 126×8 = 49 000 накладных) | 133 400 | 41 % | +| диспетчер `draw_tile` (один тайл чомпера) | 56 200 | 18 % | +| цикл `pop_process_trobs` без блитов пламени | 59 100 | 18 % | +| `pop_loose_tick` (плит в комнате НЕТ) | 28 872 | 9 % | +| heal 32×64 в `pop_chomp_redraw` | 34 000 | 11 % | +| шов / ворота соседа | 9 336 | 3 % | + +Больше половины фазы — не пиксели, а обвязка вокруг них. + +--- + +## 3. Список приёмов, отсортированный по эффекту + +### P1. Чомпер под факелом — возвращать только слой anim [модель: −160 000, 20 % работы] + +Разбор в [`perf_l11_room15.md`](perf_l11_room15.md) §3. Сейчас пометка от +факела обрабатывается как `heal 32×64 + полный draw_tile` (190 260 тактов на +единственный перерисованный тайл кадра); оригинал в этом случае рисует +ТОЛЬКО `draw_tile_anim` — графику чомпера поверх свежего пламени. + +Останется 1-2 блита челюстей ≈ 20 000-33 000. **Риск низкий**: это +сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку +«фон трогали» вокруг тайла чомпера — см. P3. + +Новое, найдено 2026-08-19. + +### P2. Логика двух Char — 152 190 тактов БЕЗ единого блита [гипотеза: −40 000 … −70 000] + +`m1→C`: `kid_tick`, `pop_phys_tick`, `pop_guard_tick`, `pop_guard_phys_tick`, +боёвка, `pop_check_can_guard_see_kid`. Графики там нет вообще, а стоит это +19 % работы кадра — столько же, сколько вся отрисовка обоих персонажей. + +**Сначала замер** (зонды внутрь синей фазы, нужна пересборка), потом решение. +Кандидаты по прошлым находкам: `sdcc_z80_stack_locals_hot_loop` (локали в +IX-фрейме), деления в луче видимости, банковые трамплины на каждый шаг. + +Новое, найдено 2026-08-19. + +### P3. Персонажи будятся каждый кадр [модель: −70 000, следствие P1] + +heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то, +что `pop_char_skip_mask` не может пропустить ни Кида, ни стража: фон трогают +каждый кадр. + +- **Кида** спасает P1: широкая пометка вокруг чомпера исчезнет; +- **стража спасти нельзя** — пламя правого факела (0,7) рисуется в ячейке + (0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже + перерисовывает. + +### P4. Накладные блита — четыре независимые правки [модель: −28 000 в 11/15; больше в сценах с 20+ блитами] + +| правка | на блит | источник | +|---|---:|---| +| `pop_cd_touch`: `uint8_t` вместо `int` для `y`/`w`/`h`, ранний выход пакетного пути | ~−1 300 | новое | +| один `gfx_w0_map`/`unmap` на ГРУППУ блитов | ~−1 350 | C5 / backlog §3 | +| размеры ленты из каталога, без `atlas_image` и чтения шапки | ~−670 | C6 / backlog §2 | +| `pop_blit_b`: аргументы в 8 бит, где хватает | ~−400 | новое | + +Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из +6 126 фиксированных. + +### P5. `pop_loose_tick` при пустой комнате — 28 872 [замер: −28 000] + +В комнате 15 нет ни одной loose-плиты, а фаза всё равно платит 9 % зелёной. +Записано в `perf_backlog.md` §7 как 27 438 — цифра подтвердилась в другой +сцене, значит это постоянная плата, а не особенность 13/23. + +Нужен ранний выход по «в комнате нет плит и нет летящих кусков». + +### P6. Цикл `pop_process_trobs` — 59 100 без блитов пламени [гипотеза: −20 000 … −40 000] + +Префетч кодов тайлов делает `pop_level_access_begin/end` (маппинг окна 0) +каждый кадр; дальше switch по всем trob'ам комнаты. Надо померить, сколько +там trob'ов и куда уходит время. + +Новое, найдено 2026-08-19. + +### P7. G5. Раскол `draw_tile` на узкие части [оценка дока: −50 000 … −60 000] + +Диспетчер + контекст оплачиваются целиком всегда; у оригинала это девять +независимых функций. В 11/15 это те самые 56 200 на один тайл — но если +сделан P1, `draw_tile` чомпера вообще не вызывается, и здесь эффект пропадёт. +Ценность приёма — в ДРУГИХ сценах (13/23, любая комната с плитами). + +**Риск средний**: в `draw_tile` собрано много инвариантов (BUG-LOOSE-3, +BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным заходом с прогоном +всех уровней. + +### P8. HEAL-WIDTH — ширина heal'ов по фактическому следу [оценка: 5-6 % цены heal'ов] + +ОБЯЗАТЕЛЬНАЯ по решению пользователя (2026-08-18). След плиты 58 px в +подземелье / 57 во дворце против используемых 60 и 64. +Постановка — `TASKS_OPEN.md`, якорь `heal-width`. + +### P9. G8 — пометку СОСЕДА ставить узкой полосой (28 px), а не тайлом [гипотеза] + +Парная к HEAL-WIDTH. Относится к сценам с падающими плитами (13/23), в 11/15 +не играет. Разбор — `perf_green_phase.md` §G8, там же три условия, из-за +которых это не «просто уменьшить число». + +### P10. Футпринт персонажа — из физики, а не считать заново [замер: −11 574 на fore-проход] + +`backlog` §1. С двумя персонажами — ~23 000 за кадр. Мешает то, что физика +(банк 3) держит `char_col_left/right` в статиках, а слой фона — банк 2. +**Риск средний**: окно fore-клипа заводилось под клинок и брызги. + +### P11. Мелочи с известной ценой [замер, `backlog` §7] + +| что | цена | где | +|---|---:|---| +| `pop_clip_char_top` — трамплин банк 4 → банк 3 ради одной проверки | 8 892 | `pop_cdraw.c` | +| `cd_sig_make` + возврат из `pop_char_draw` | 7 944 | `pop_cdraw.c` | +| `pop_loadkid` + расчёт координат кадра | 7 410 | `pop_cdraw.c` | +| `obj_x * 8 / 7` — последнее `__divsint` в горячем пути | ~2 400 | `pop_char_draw` | + +### P12. G9 — снять временную оснастку [замер: −6 000] + +`pop_dbg_b1..b6` в `pop_blit_b` (~400 на блит), `pop_dbg_kind`/`m16`, +`pop_dbg_m5..m15`, счётчик `rd_cnt` в `pop_redraw_needed`. +**Только ПОСЛЕ окончания оптимизации** — без них не мерить. + +### P13. Крупные рефакторинги — брать, только если понадобится ещё запас + +- **C7 / backlog §5-6: objtable + отложенные таблицы back/mid/fore.** У + оригинала «посетить тайл» стоит копейки, потому что таблицы только копят + записи, а рисует один `draw_table()` в конце. У нас блит идёт сразу из + обхода, и fore-проход отдельный НА КАЖДОГО персонажа. +- **backlog §4: единый проход по тайлам вместо трёх** (`pop_redraw_needed`, + `pop_process_trobs`, `pop_fore_over_char`) и семь счётчиков причин + перерисовки вместо одного `kind`. +- **G6: меньше блитов в `RD_FLOOR`**, **G7: `mob_tick_one` в file-scope**. + +--- + +## 4. Сводка: что сколько даёт в 11/15 + +| # | приём | эффект | тип оценки | риск | +|---|---|---:|---|---| +| P1 | чомпер: только anim-слой | −160 000 | модель | низкий | +| P3 | Кид перестанет будиться | −70 000 | модель | следствие P1 | +| P2 | логика двух Char | −40 000 … −70 000 | гипотеза | ? | +| P4 | накладные блита (4 правки) | −28 000 | модель | низкий | +| P5 | `loose_tick` без плит | −28 000 | замер | низкий | +| P6 | цикл `process_trobs` | −20 000 … −40 000 | гипотеза | ? | +| P10 | футпринт из физики | −23 000 | замер | средний | +| P11 | мелочи (4 штуки) | −26 000 | замер | низкий | +| P12 | снять оснастку | −6 000 | замер | нулевой | +| P7 | раскол `draw_tile` | 0 здесь (−50 000 в 13/23) | оценка | средний | +| P8/P9 | HEAL-WIDTH / G8 | 0 здесь (сцены с плитами) | оценка | низкий/средний | + +Верхняя часть списка (P1 + P3 + P4 + P5) — **около −286 000 из 801 768, то +есть 36 % работы кадра**, и вся она низкого риска. Этого хватит, чтобы +сцена ушла с 1,86 растрового кадра до ~1,2 — но НЕ хватит, чтобы период +кадра упал с 4 растров до 3: для этого работа должна уложиться в 430 000, +то есть нужны ещё ~90 000 сверху (P2 или P6). diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 696c0f9..9fbe32a 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -313,6 +313,11 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS. Числа там уже разобраны: 60 = 32 свой тайл + 28 собственный свес, и для запечки САМОГО тайла 60 минимальны — сужать можно только пометку СОСЕДА. +**Место в общей очереди — [`../docs/perf_registry.md`](../docs/perf_registry.md) +(позиция P8).** Там же собраны ВСЕ отложенные оптимизации из трёх фазовых +доков, отсортированные по измеренному эффекту, и разложена цена одного блита +(6 126 тактов фиксированной накладной на любой блит, независимо от размера). + ### L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние