From de68eb5cec9c9d53ab85b6032d915b624f731bbc Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 11:30:01 +0300 Subject: [PATCH] =?UTF-8?q?P1:=20=D1=87=D0=BE=D0=BC=D0=BF=D0=B5=D1=80=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B8=D1=81=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D1=81=D1=8F=20=D0=BD=D0=B5=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B7=D0=BE=D0=B9?= =?UTF-8?q?=20=E2=80=94=20=D0=BC=D0=B8=D0=BD=D1=83=D1=81=20110=20802=20?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Позиция заводилась с НЕПОЛНЫМ диагнозом. Я приписал 190 260 тактов пометке от факела (пламя лежит в ячейке правого соседа, то есть поверх чомпера, и запекается каждый кадр). Правка по этому диагнозу не дала ничего: 769 002 против 768 684. Зонд pop_dbg_kind показал факт: все 312 перерисовок прогона — вид POP_RD_CHOMP, полная, и ни одной от факела. Собственная пометка чомпера просто перебивала пометку соседа. Настоящая причина нашлась сверкой с animate_chomper (seg007:0448). Оригинал заканчивает её так: if ((curr_modifier & 0x7F) < 6) redraw_at_trob(); то есть перерисовывает чомпер только пока фаза меньше 6 — пять кадров из пятнадцати. Это не оптимизация оригинала, а следствие таблицы поз: chomper_fram1 = {3,2,0,1,4,3,3}, и с фазы 5 до конца круга поза одна и та же. Мы метили тайл каждый кадр, пока trob жив, а живёт он всё время, пока Кид в том же ряду — то есть платили полный draw_tile плюс heal 32x64 за неизменную картинку в двух третях кадров. Сделано: 1. пометка только при фазе < 6; на фазе 5 — обе страницы дабл-буфера (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу пометка догоняет в кадре фазы 6, где поза та же — CHOMP_FRAM1[6] == 3); 2. новый вид POP_RD_CHOMP_ANIM -> pop_chomp_anim_draw: три блита графики чомпера поверх свежего пламени, без heal и без остальных слоёв — порт ветки redraw_frames_anim (seg008:0211), где оригинал делает ровно draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim и никакого wipe; 3. приоритет полной перерисовки над anim в pop_set_redraw: у оригинала это два независимых счётчика и full побеждает, а у нас вид один на тайл, и без проверки исход решал бы порядок trob'ов в списке. Обе половины работают — замер даёт 40 % полных перерисовок и 60 % лёгких. Работа 768 684 -> 657 882 (медиана), зелёная 294 510 -> 183 420. В 40 % кадров цена прежняя: там поза реально меняется, это честная работа. Циан не сдвинулся ни на такт, то есть надежда P3 (Кид перестанет будиться каждый кадр) пока не оправдалась — метки продолжают его будить. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/perf_l11_room15.md | 60 ++++++++++++++++++++++++ applications/PoP/docs/perf_registry.md | 58 ++++++++++++++++++----- applications/PoP/roomtest/pop_bg.h | 11 +++++ applications/PoP/roomtest/pop_redraw.c | 15 +++++- applications/PoP/roomtest/pop_redraw.h | 1 + applications/PoP/roomtest/pop_room.c | 41 ++++++++++++++++ applications/PoP/roomtest/pop_trob.c | 32 ++++++++++--- 7 files changed, 198 insertions(+), 20 deletions(-) diff --git a/applications/PoP/docs/perf_l11_room15.md b/applications/PoP/docs/perf_l11_room15.md index 5faa46a..52d25fc 100644 --- a/applications/PoP/docs/perf_l11_room15.md +++ b/applications/PoP/docs/perf_l11_room15.md @@ -57,6 +57,14 @@ `pop_dbg_rdmax_tot` = **1**: за кадр перерисовывается РОВНО ОДИН тайл, и стоит он все 190 260 тактов зелёной фазы. +> **ПОПРАВКА 2026-08-19 (после реализации P1).** Механизм ниже описан +> верно, но ГЛАВНЫМ источником 190 260 тактов он НЕ был. Зонд +> `pop_dbg_kind` показал, что все 312 перерисовок в прогоне — вид +> `POP_RD_CHOMP` (полная), и ни одной от факела: собственная пометка +> чомпера просто перебивала пометку соседа. Настоящая причина — в §6. +> Урок ровно тот, что уже записан в `defer_unexplained_quirks`: механизм, +> который правдоподобно объясняет цифру, ещё не доказан цифрой. + Цепочка: 1. `TORCH_ANIM_DIV = 1` — факел меняет кадр пламени КАЖДЫЙ логический кадр; @@ -103,6 +111,56 @@ heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ — та же болезнь (полный тайл там, где хватает полосы), но на другом материале. +## 6. Настоящая причина 190 260 тактов: перерисовка неизменной позы + +Найдено при реализации P1, сверкой с `animate_chomper` (seg007:0448). +Функция оригинала заканчивается так: + +```c +if ((curr_modifier & 0x7F) < 6) { + redraw_at_trob(); +} +``` + +То есть чомпер перерисовывается **только пока фаза меньше 6** — пять кадров +из пятнадцати (`POP_CHOMPER_SPEED = 15`). Это не оптимизация оригинала, а +следствие таблицы поз: `chomper_fram1 = {3,2,0,1,4,3,3}`, и начиная с фазы 5 +и до конца круга поза одна и та же — 3. Рисовать её десять кадров подряд +значит рисовать ровно ту же картинку. + +Мы же метили тайл БЕЗУСЛОВНО, каждый кадр, пока trob жив — то есть платили +полный `draw_tile` плюс heal 32×64 за неизменную картинку в двух третях +кадров. А trob у чомпера живёт, пока Кид в том же РЯДУ (`animate_chomper` +снимает его только при фазе ≥ 6 и ушедшем Киде) — в 11/15 Кид стоит в (0,2), +чомпер в (0,3), ряд один. + +**Что сделано:** + +1. пометка только при фазе < 6, и на фазе 5 — на ОБЕ страницы дабл-буфера + (она последняя рисуемая, её поза обязана лечь на обе; вторую страницу + пометка догоняет в кадре фазы 6, где поза та же самая); +2. пометка от факела (`set_redraw_anim_right`) переведена на новый вид + `POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw`: три блита графики чомпера + поверх свежего пламени, без heal и без остальных слоёв — порт ветки + `redraw_frames_anim` (seg008:0211); +3. приоритет полной перерисовки над anim в `pop_set_redraw` — у оригинала + это два независимых счётчика, и `full` побеждает. + +**Результат** (замер, 552 кадра): полная перерисовка теперь в **40 %** +кадров, лёгкий возврат челюстей — в 60 %. Зелёная фаза: 291 888 в дорогом +кадре против 183 414 в дешёвом. + +| | работа | зелёная | +|---|---:|---:| +| до P1 | 768 684 | 294 510 | +| после P1, медиана | **657 882** | **183 420** | +| после P1, дорогой кадр (40 %) | 765 936 | 291 888 | + +**−110 802 на медиане** при ожидании −160 000. Разница в том, что 40 % +кадров по-прежнему платят полную цену: там поза реально меняется, и это уже +не лишняя работа, а честная. Дальше её можно резать только раскладом +`draw_tile` на части (P7) или сужением heal (P8). + ## 5. Журнал правок по этой сцене | дата | правка | работа | синяя | зелёная | циан | @@ -111,6 +169,8 @@ heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ | 2026-08-19 | **P5**: гейты холостого хода в `pop_loose_tick` | **767 928** | 285 864 | 294 384 | 187 764 | | | | −33 840 | −7 374 | −26 532 | +6 | | 2026-08-19 | зонды для замера P2/P6 (временные) | 768 684 | 286 518 | 294 510 | 187 761 | +| 2026-08-19 | **P1**: чомпер — перерисовка только при фазе < 6 (медиана) | **657 882** | 286 503 | 183 420 | 187 761 | +| | | −110 802 | −15 | −111 090 | 0 | Оснастка P2/P6 стоит 756 тактов на кадр — замеры до и после сопоставимы. diff --git a/applications/PoP/docs/perf_registry.md b/applications/PoP/docs/perf_registry.md index 396fdd0..a2da29e 100644 --- a/applications/PoP/docs/perf_registry.md +++ b/applications/PoP/docs/perf_registry.md @@ -72,7 +72,30 @@ ## 3. Список приёмов, отсортированный по эффекту -### P1. Чомпер под факелом — возвращать только слой anim [модель: −160 000, 20 % работы] +### P1. Чомпер: перерисовка неизменной позы ✅ СДЕЛАНО 2026-08-19 — −110 802 + +**Диагноз, с которым позиция заводилась, оказался неполным.** Я приписал +190 260 тактов пометке от факела; зонд `pop_dbg_kind` показал, что все 312 +перерисовок прогона — вид `POP_RD_CHOMP` (полная), а пометку соседа она +просто перебивала. Настоящая причина нашлась сверкой с `animate_chomper` +(seg007:0448): оригинал перерисовывает чомпер **только при фазе < 6**, пять +кадров из пятнадцати, потому что с фазы 5 поза не меняется +(`chomper_fram1 = {3,2,0,1,4,3,3}`). Мы метили тайл каждый кадр. + +Сделано три вещи: условие фазы (с пометкой обеих страниц на фазе 5), новый +вид `POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw` (три блита поверх огня, порт +ветки `redraw_frames_anim`) и приоритет полной перерисовки над anim в +`pop_set_redraw`. Обе половины работают: замер даёт 40 % полных +перерисовок и 60 % лёгких. + +Работа 768 684 → **657 882** (медиана), зелёная 294 510 → **183 420**. +В 40 % кадров цена осталась прежней — там поза действительно меняется, и это +уже честная работа; резать её дальше только через P7 (раскол `draw_tile`) +или P8 (ширина heal). + +Полный разбор — [`perf_l11_room15.md`](perf_l11_room15.md) §6. + +
Исходная (неполная) постановка Разбор в [`perf_l11_room15.md`](perf_l11_room15.md) §3. Сейчас пометка от факела обрабатывается как `heal 32×64 + полный draw_tile` (190 260 тактов на @@ -83,7 +106,7 @@ сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку «фон трогали» вокруг тайла чомпера — см. P3. -Новое, найдено 2026-08-19. +
### P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась @@ -279,7 +302,7 @@ BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным за | P5 | `loose_tick`: гейты холостого хода | ✅ **сделано** 2026-08-19 | −33 840 на кадре (ждали −28 000) | | P2 | замер синей фазы | ✅ замерено 2026-08-19 | гипотеза «трамплины» отвергнута | | P6 | замер `process_trobs` | ✅ замерено 2026-08-19 | 11 058 префетч + ~43 000 цикл | -| P1 | чомпер под факелом: только слой anim | ⬜ **следующее** | ждём −160 000 | +| P1 | чомпер: перерисовка только при фазе < 6 | ✅ **сделано** 2026-08-19 | −110 802 (ждали −160 000) | | P2b | гейт луча видимости стража | ⬜ | ждём −30 000, риск низкий | | P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 | | P11 | мелочи с известной ценой | ⬜ | ждём −26 000 | @@ -297,30 +320,39 @@ BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным за | источник | ожидание | |---|---:| -| P1 (чомпер: только слой anim) | −160 000 | -| P3 (Кид перестанет будиться, следствие P1) | −70 000 | +| ~~P1 (чомпер)~~ | ~~−160 000~~ → **сделано, −110 802** | +| P3 (Кид перестанет будиться) | −70 000 — **под вопросом**, см. ниже | | P2b (гейт луча видимости) | −30 000 | | P4 (накладные блита) | −28 000 | | P11 (мелочи) | −26 000 | | P10 (футпринт из физики) | −23 000 | | P6a+P6b | −20 000 | -| **сумма** | **−357 000** | +| **сумма оставшегося** | **−197 000** | -То есть цель достижима, но **впритык и только если сделать ВСЁ перечисленное**, -причём половина суммы держится на одной позиции (P1). Запаса нет, и любая -позиция, давшая меньше ожидаемого, возвращает нас к разбору `pop_phys_tick` -(P2a) — 105 246 тактов на физику двух НЕПОДВИЖНЫХ персонажей, где цена -измерена, а причина ещё не найдена. +**После P1 картина изменилась к худшему.** Снять с дорогого кадра надо +336 000, а всё оставшееся в списке даёт 197 000 — и это при оптимистичных +оценках. Вдобавок P3 (Кид перестанет будиться) теперь под вопросом: циан +после P1 не сдвинулся ни на такт (187 761), то есть метки от чомпера и +факелов продолжают будить обоих персонажей в каждом кадре. + +Значит без **P2a — разбора `pop_phys_tick`** (105 246 тактов на физику двух +НЕПОДВИЖНЫХ персонажей, цена измерена, причина не найдена) период 3 растра +не берётся. Это и есть следующая крупная позиция. **Текущее состояние бюджета 11/15:** | | работа | синяя | зелёная | циан | период | |---|---:|---:|---:|---:|---:| | до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра | -| после P5 | **767 928** | 285 864 | 294 384 | 187 764 | 4 растра | +| после P5 | 767 928 | 285 864 | 294 384 | 187 764 | 4 растра | +| после P1 (медиана) | **657 882** | 286 503 | 183 420 | 187 761 | 4 растра | +| после P1 (дорогой кадр, 40 %) | 765 936 | 286 503 | 291 888 | 187 761 | 4 растра | Цель — уложить РАБОТУ в 430 000 (тогда период станет 3 растра вместо 4; -хвост кадра — три `gfx_wait_vsync`). Осталось снять **338 000**. +хвост кадра — три `gfx_wait_vsync`). Осталось снять **228 000** с медианы +и **336 000** с дорогого кадра. Считать надо по ДОРОГОМУ: период задаётся +каждым кадром отдельно, и пока дорогие кадры не влезут, 40 % кадров будут +идти по 4 растра. ### Как воспроизвести сцену (важно для следующей сессии) diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index caeb147..9d27417 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -124,6 +124,17 @@ void pop_spike_redraw(int row, int col) __banked; * поэтому соседа перерисовывать не надо). */ void pop_chomp_redraw(int row, int col) __banked; +/* Вернуть ТОЛЬКО слой anim чомпера — его собственную графику поверх свежего + * пламени соседнего факела. Порт ветки redraw_frames_anim в redraw_needed + * (seg008:0211): оригинал на такую пометку делает ровно три вызова — + * draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim — и НЕ + * делает ни wipe, ни полного draw_tile (wipe у него отдельный счётчик). + * + * Из этих трёх нам нужен один: topright — про верх ВОРОТ под пустой + * клеткой, к чомперу не относится, а anim_right (пламя левого соседа) у нас + * уже нарисован — pop_torch_draw запекает его в фон ДО redraw_needed. */ +void pop_chomp_anim_draw(int row, int col) __banked; + /* Анимация факела/зелья (динамика каждый кадр; фон запечён без них): * modif — живой room_modif тайла. Для факела col — колонка САМОГО факела * (пламя рисуется в ячейке правого соседа). */ diff --git a/applications/PoP/roomtest/pop_redraw.c b/applications/PoP/roomtest/pop_redraw.c index a0eba7c..d8e3018 100644 --- a/applications/PoP/roomtest/pop_redraw.c +++ b/applications/PoP/roomtest/pop_redraw.c @@ -88,7 +88,18 @@ void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked if (tilepos >= NTILES) return; if (!rd_cnt[tilepos]) rd_pending++; else pop_bake_slot_reset(tilepos); /* перепометка — копия запечки не годится */ - rd_kind[tilepos] = kind; + /* ПРИОРИТЕТ ПОЛНОЙ ПЕРЕРИСОВКИ НАД слоем anim. У оригинала это не + * конфликт видов, а два независимых счётчика, и полная побеждает + * (redraw_needed, seg008:0211: `if (redraw_frames_full) draw_tile(); + * else if (redraw_frames_anim) {...}`). У нас вид ОДИН на тайл, и без + * этой проверки исход решал бы порядок trob'ов в списке: чомпер, который + * СЕЙЧАС смыкает челюсти, ставит POP_RD_CHOMP (ему нужен heal — поза + * меняется), а факел слева тут же метил бы тот же тайл как + * POP_RD_CHOMP_ANIM, и heal пропал бы — новая поза легла бы поверх + * старой. */ + if (!(kind == POP_RD_CHOMP_ANIM && rd_kind[tilepos] == POP_RD_CHOMP && + rd_cnt[tilepos])) + rd_kind[tilepos] = kind; if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages; } @@ -112,6 +123,7 @@ void pop_redraw_needed(void) __banked if (!rd_pending) return; for (i = 0; i < NTILES; i++) { if (rd_cnt[i]) { + pop_dbg_kind(rd_kind[i]); /* ВРЕМЕННО: какой вид перерисовки */ switch (rd_kind[i]) { case POP_RD_SPIKE: pop_spike_redraw(row, col); break; case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break; @@ -120,6 +132,7 @@ void pop_redraw_needed(void) __banked case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break; case POP_RD_GATE: pop_gate_redraw(row, col); break; case POP_RD_CHOMP: pop_chomp_redraw(row, col); break; + case POP_RD_CHOMP_ANIM: pop_chomp_anim_draw(row, col); break; default: break; } cnt++; /* ВРЕМЕННО: замер */ diff --git a/applications/PoP/roomtest/pop_redraw.h b/applications/PoP/roomtest/pop_redraw.h index 11a47a5..7ab303f 100644 --- a/applications/PoP/roomtest/pop_redraw.h +++ b/applications/PoP/roomtest/pop_redraw.h @@ -31,6 +31,7 @@ #define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */ #define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */ #define POP_RD_CHOMP 7 /* чомпер: кадр смыкания челюстей */ +#define POP_RD_CHOMP_ANIM 8 /* чомпер: вернуть ТОЛЬКО слой anim поверх огня */ /* Виды перерисовки полосы у потолка (по колонке). */ #define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */ diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 9b98842..3cd5424 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -626,6 +626,47 @@ void pop_loose_shake_draw(int row, int col) __banked gfx_set_bank(GFX_BANK_NORMAL); } +/* Вернуть ТОЛЬКО слой anim чомпера — порт ветки redraw_frames_anim в + * redraw_needed (seg008:0211). Контракт и разбор — в шапке объявления + * (pop_bg.h). + * + * Чем это отличается от pop_chomp_redraw и почему нужна отдельная функция. + * Пометку ставит АНИМАЦИЯ ФАКЕЛА: пламя лежит в ячейке ПРАВОГО соседа + * (seg008:560), то есть поверх чомпера, и запекается в фон каждый кадр. + * Возвращать после него надо ровно графику чомпера — а pop_chomp_redraw + * делал heal 32x64 плюс ПОЛНЫЙ draw_tile, то есть пересобирал тайл со всеми + * слоями (правая грань, база, низ, loose), которых пламя не касалось. + * Замер 11/15 (2026-08-19): 190 260 тактов на этот единственный тайл, 24 % + * работы кадра. + * + * heal не нужен: поза чомпера здесь НЕ меняется (свою анимацию ведёт + * pop_chomp_redraw по своей пометке), стирать нечего — рисуем ту же + * графику поверх свежего пламени. Оригинал на пометку anim wipe тоже не + * делает: у него это отдельный счётчик wipe_frames. + * + * Передний слой (зубья, POP_CHOMP_FRAM_FOR) сюда НЕ входит — как и в + * оригинале, где fore идёт по своему счётчику redraw_frames_fore. Геометрия + * это подтверждает: пламя занимает 18 строк с низом на 63*row+22, передние + * зубья — на dmy = 63*row+62, то есть на 40 строк ниже; пересекается с огнём + * только ВЕРХНЯЯ челюсть (подъём до 0x32 = 50 px), а она в back-слое. */ +void pop_chomp_anim_draw(int row, int col) __banked +{ + int x = POP_COL_XH[col] * 8; + int dmy = 63 * row + 62; /* dby - 3, как в draw_tile */ + uint8_t cm = pop_tile_mod(row, col); + uint8_t pose = pop_chomp_pose(cm); + + gfx_set_bank(GFX_BANK_SPRITE); + pop_cd_batch_begin(); /* одна пометка на все куски */ + pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy); + if (cm & 0x80) /* кого-то перемололо */ + pop_env_b((uint8_t)(pose + 114), x + 8, dmy - 6); + if (CHOMP_FRAM_TOP[pose]) + pop_env_b(CHOMP_FRAM_TOP[pose], x, dmy - CHOMP_FRAM_Y[pose]); + pop_cd_batch_end(); + gfx_set_bank(GFX_BANK_NORMAL); +} + /* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 * читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 8585e08..6f9e9a0 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -730,7 +730,7 @@ void pop_process_trobs(uint8_t cur_room) __banked * случай «застыл справа от факела» в BUGS_OPEN.md как открытый: * пики и меч рядом с факелом на уровнях 1-4 не встретились. */ if (trob_rcode[i] == TILE_CHOMP) - pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP, 1); + pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP_ANIM, 1); } } if (room == cur_room && code == TILE_SPIKE) { @@ -749,11 +749,31 @@ void pop_process_trobs(uint8_t cur_room) __banked pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1)); } if (room == cur_room && code == TILE_CHOMP) { - /* Кадр меняется каждый тик, пока trob жив — как у пик, метим - * текущую страницу; последний кадр (челюсти встали) — обе, - * иначе на второй странице дабл-буфера застынет предыдущая поза - * и чомпер дрожит через кадр. */ - pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(type < 0 ? 2 : 1)); + /* ТОЛЬКО ПОКА ФАЗА < 6 — как в оригинале (animate_chomper, + * seg007:0448 заканчивается `if ((curr_modifier & 0x7F) < 6) + * redraw_at_trob();`). + * + * Это не оптимизация оригинала, а точное следствие таблицы поз: + * CHOMP_FRAM1 = {3,2,0,1,4,3,3}, то есть с фазы 5 и до конца + * круга (POP_CHOMPER_SPEED = 15) поза одна и та же — 3. + * Перерисовывать её десять кадров подряд значит рисовать ровно + * ту же картинку, а стоит это 190 260 тактов НА КАДР: полный + * draw_tile плюс heal 32x64, 24 % работы кадра в 11/15 + * (замер 2026-08-19). + * + * На фазе 5 метим ОБЕ страницы дабл-буфера: она последняя + * рисуемая, и её поза обязана лечь на обе, иначе на второй + * останется поза фазы 4 и чомпер будет дрожать через кадр. + * Сходится и по кадрам: вторую страницу эта пометка догоняет в + * кадре фазы 6, где поза та же самая (CHOMP_FRAM1[6] == 3). + * + * Про type < 0 отдельной ветки больше нет: trob снимается сам + * только при frame >= 6 (см. animate_chomper выше), то есть + * когда перерисовки уже не идут, а на экране с фазы 5 лежит + * финальная поза на обеих страницах. */ + uint8_t ph = (uint8_t)(mod[tp] & 0x7F); + if (ph < 6) + pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(ph == 5 ? 2 : 1)); } if (room == cur_room && code == TILE_GATE) { /* draw_trob (seg007:01E6), которым заканчивается animate_door: