Files
Sprinter-SDCC/applications/PoP/docs/l9_invert_plan.md
T
snark13 f541c0aad9 L9-INVERT: клип поля при перевороте (мусор в бортах)
Найдено пользователем: после телепорта в перевёрнутом виде ниже поля мусор.
Спрайты, торчащие в обычном виде ВЫШЕ поля (полоса кладки у потолка), после
отражения торчат НИЖЕ и лезут в борт с полосой HP.

blit_b_clip при перевороте режет по обеим границам поля всегда, а не по
pop_t_clip_top; быстрый путь pop_blit_b отдаёт клипующему всё, что выходит
за поле.

Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
комнаты (факелы, чомперы, пол) зеркальны.  Переходы между комнатами в
перевёрнутом виде работают.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:16:27 +03:00

429 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# L9-INVERT — план реализации зелья переворота (уровень 9)
Рабочий план, по которому задача делается с чистого контекста. Доска —
[`../roomtest/TASKS_OPEN.md#l9-invert`](../roomtest/TASKS_OPEN.md); правила
подпроекта — `../CLAUDE.md` (SDLPoP = источник истины, диагноз железа —
только артефактом).
## 0. Что и зачем
Зелья типа 4 на уровне 9 (комната 7 тайл `(1,7)`, комната 10 тайл `(0,4)`)
переворачивают картинку вверх ногами. В оригинале это `toggle_upside()`
(seg000:15E9): `upside_down = ~upside_down`, `need_redraw_because_flipped = 1`.
Больше зелье не делает НИЧЕГО — ни вспышки, ни урона (seg006:1885, ветка
`case 4`). Снимается: смертью Кида (seg000:1224, при `alive >= 0`) и стартом
уровня (seg003:38/188). Управление НЕ инвертируется. Второе зелье
переворачивает обратно.
Оригинал переворачивает готовый offscreen на выводе (`flip_screen` перед
копированием прямоугольников на экран, seg000:939/946). Нам этот путь
закрыт: страниц ровно две (`gfx_set_visible_page` → ESTEX $54 SELPAGE, бит 0),
рабочей третьей нет. Поэтому:
- **уже нарисованное** переворачиваем ОДИН РАЗ построчной копией
акселератора (решение пользователя 2026-08-12);
- **всё, что рисуется дальше**, рисуем зеркально: фон (row-major) — новыми
vflip-блитами, персонажи (column-major) — заранее подготовленными
зеркальными кадрами.
Полоса HP и лейбл комнаты живут в борту (`y = 194 + POP_YOFF`, поле —
`POP_PLAYFIELD_H = 192`) и не переворачиваются, как и в оригинале.
---
# Часть I — libbgi
## I.0 РАЗВЕДКА ЖЕЛЕЗА — ЗАКРЫТА 2026-08-12
**Оба факта подтверждены, ядро и обёртка написаны и проверены в MAME**
(`tests/pageflip`, 5/5 PASS — прямая копия, vflip, целость рамки, широкая
копия 320 = два прохода, неприкосновенность источника).
- **Р1 подтверждён**: Port_Y можно менять между read- и write-триггером,
если ПЕРЕД вторым OUT стоит STOP (`LD B,B`) — он разоружает accel, и fetch
immediate-операнда OUT уже безопасен. Приём не новый: ровно так работает
`_bgi_scroll_cols_raw` (ядро `gfx_scroll_v`), то есть он был в проде
задолго до этой задачи — указал пользователь. Значит и обходной путь
через ОЗУ-буфер не нужен;
- **Р2 подтверждён**: обе страницы адресуемы одновременно, копия между ними
идёт по разнице баз (`_gfx_addr_shadow_base` / `_gfx_addr_base`), строку
выбирает Port_Y.
Сделано: `libbgi/bgi256/_bgi_flip_rows_raw.c` (ядро) +
`libbgi/common/gfx_copy_page.c` (обёртка, режимы `GFX_COPY_DIRECT` /
`GFX_COPY_VFLIP`) + `tests/pageflip`. Пункты I.1 и I.3 ниже — ЗАКРЫТЫ этим
же коммитом; описание оставлено как контракт.
### Исходный текст разведки (для истории)
Два факта, на которых стоит вся схема, сейчас НЕ подтверждены артефактом.
Пока их нет, остальные пункты не начинать (memory `defer_unexplained_quirks`).
**Р1. Смена Port_Y между burst-чтением и burst-записью.** Схема требует:
армировать копию, `LD A,(HL)` (burst-чтение строки src), сменить Y,
`LD (DE),A` (burst-запись в другую строку). Между триггерами НЕЛЬЗЯ
выполнять инструкции, читающие ОПЕРАНД из памяти — fetch операнда
перезабивает буфер акселератора кодом (memory `accel_operand_fetch_retrigger`).
Значит:
- смена порта — только `out (c),a` (ED 79, регистровая форма); `out (#89),a`
(D3 89) читает immediate байт и УБЬЁТ буфер;
- новое значение Y — только из регистра (`ld a,d`), не `ld a,#n` и не из
памяти;
- `C` = 0x89 и оба значения Y должны лежать в регистрах ДО триггера чтения.
Проверка: `tests/accflip` по образцу `tests/accop` — заполнить строку
источника известным паттерном, скопировать в другую строку со сменой Y,
прочитать VRAM побайтно и сравнить. Отрицательный результат — стоп-сигнал:
переворот придётся делать через промежуточный ОЗУ-буфер (строка 320 Б), и
бюджет вырастет примерно вдвое.
**Р2. Адресация двух страниц в одном окне W3.** `gfx.h` утверждает, что
страница 1 «начинается на 320 байт дальше (0xC140)», но это комментарий, а не
замер. Нужно снять дампом: как адрес строки складывается из Port_Y и
смещения в окне, и видны ли обе страницы одновременно. От ответа зависит,
делается ли копия A→B одним проходом или через смену видеобанка.
Итог разведки записать в `libbgi/docs` (или `docs/new/06-accel.md`
дополнением) и в memory — это знание переиспользуется во всех будущих
экранных эффектах.
## I.1 Ядро копии с реверсом Y
`libbgi/bgi256/_bgi_copy_rows_raw.c` умеет только ИНКРЕМЕНТ Port_Y на строку
(`y0` + шаг вперёд; страйды патчатся SMC при входе). Нужен вариант, где
одна сторона идёт вниз, другая вверх.
- предпочтительно: новый leaf `_bgi_flip_rows_raw.c` (правило «1 функция =
1 модуль»), а не флаг в существующем — горячий путь блиттера трогать не
надо, и DCE не потащит лишнего в программы без переворота;
- вход тот же (`__sdcccall(1)`: src→HL, dst→DE, дальше стек), плюс
`y0_src` / `y0_dst` и признак направления;
- DI-окно — одна строка, как в остальных ядрах (арминг в каждой скобке:
в окне EI между чанками CBL-ISR армирует акселератор своим размером,
см. `docs/accel-fill-budget.md`).
## I.2 Публичные vflip-блиты для row-major — СДЕЛАНО 2026-08-12
Ассемблера не потребовалось вовсе: у row-major картинки вертикальное
зеркало — это порядок строк, а он задаётся ЗНАКОМ src-страйда, который
`_bgi_blit_rows_raw` и так принимает знаковым (`int sstride`) и патчит SMC
в adc-цепочку. Обёртки просто дают адрес ПОСЛЕДНЕЙ строки и `-stride`,
цена кадра та же, что у обычного блита.
Написаны `gfx_blit_noclip_vflip`, `gfx_blit_part_noclip_vflip`,
`gfx_blit_part_vflip`; `tests/pageflip` расширен до 8 проверок, все PASS.
Грабли, которые поймал тест: у клипающего варианта первая строка источника
обязана считаться от высоты ДО клипа (`h0`), иначе обрезка сверху экрана
сдвигает картинку — сначала формула брала уже укороченную `h`.
### Контракт (исходное описание)
Все row-major блиты приложения идут ровно через три функции (`pop_tile.c:281`,
`:283`, `:286`, `:325`, `:327`):
| есть | нужен vflip-двойник |
|---|---|
| `gfx_blit_noclip` | `gfx_blit_noclip_vflip` |
| `gfx_blit_part_noclip` | `gfx_blit_part_noclip_vflip` |
| `gfx_blit_part` (с клипом) | `gfx_blit_part_vflip` |
Семантика: `(x,y)` — левый ВЕРХНИЙ угол результата на экране (как у обычных
блитов), строки источника выводятся снизу вверх. Это позволяет вызывающему
не менять арифметику позиции, а только пересчитать `y` под поле.
`gfx_blit_cols*` (column-major) vflip-двойников НЕ получают: реверс идёт
внутри колонки, а accel копирует блок только вперёд. Для персонажей —
часть II.4.
`gfx_heal*` не трогаем: heal симметричен, он восстанавливает прямоугольник из
ОЗУ-копии по экранным координатам, а координаты вызывающий уже даёт
перевёрнутые.
Тест: `tests/blitvflip` — эталонный спрайт, побайтная сверка VRAM.
## I.3 Копия прямоугольника экран→экран
```c
#define GFX_COPY_DIRECT 0
#define GFX_COPY_VFLIP 1
void gfx_copy_rect(uint8_t src_page, int sx, int sy,
uint8_t dst_page, int dx, int dy,
uint16_t w, uint16_t h, uint8_t mode);
```
- `w > 256` режется на burst'ы вызывающим ядром (320 = 160+160 либо 256+64 —
выбрать по замеру, разницы в тактах на строку почти нет);
- **горизонтального флипа не будет**: accel копирует блок только вперёд,
побайтовый реверс дал бы 320 burst'ов на строку вместо двух. Если он
когда-нибудь понадобится — только через ОЗУ-буфер с CPU-реверсом;
- клип не нужен (вызывающий гарантирует границы), но проверка «прямоугольник
в пределах экрана» в safe-варианте библиотеки — обязательна.
Тест: `tests/pagecopy` — прямой режим и VFLIP, побайтная сверка.
## I.4 Чего в списке пользователя не хватало
1. **Размерный регресс**: после каждого шага `make size-check`; новые модули
не должны утянуть за собой рост в программах, которые их не зовут (проверка
на `examples/` и `tests/`).
2. **Обе сборки библиотеки** — fast и safe (`sprinter-cc --safe`): гарды
параметров в safe, критичные — в обеих.
3. **Документация**: `docs/sprite-api-design.md` (раздел про блиттер),
`docs/TODO.md`, справочник API; факты разведки I.0 — в `docs/new/06-accel.md`.
4. **IM2/CBL**: длинные серии коротких DI-окон не должны ломать кадровые
прерывания и клавиатуру — проверить `tests/kbdpoll`-сценарием после
внедрения (клавиатура вычерпывается idle-хуком, а переворот идёт вне
ожидания кадра).
5. **Порядок «страница ↔ ОЗУ-копия»**: копия должна идти банком
`GFX_BANK_NORMAL`, иначе перевёрнутый фон не попадёт в ОЗУ-копию и heal
начнёт восстанавливать старую картинку. Это ключевой инвариант всей
схемы — вынести отдельным утверждением в тест.
---
# Часть II — приложение (roomtest)
## II.1 Состояние и момент переключения — СДЕЛАНО 2026-08-12
Проверено в MAME (уровень 9, комната 7): нажатие чита переворачивает всю
комнату целиком за один кадр, повторное — возвращает. Персонажи и
анимируемые тайлы (факелы) пока рисуются НЕ зеркально — это шаги II.3/II.4.
Что появилось:
- `pop_upside` + `pop_upside_dirty` (порт `upside_down` и
`need_redraw_because_flipped`) в `pop_map.c`;
- ветка `case 4` в `pop_proc_get_object`: только тоггл, без вспышки и урона
(как seg006:1885);
- сброс в `pop_start_level` (seg003:38/188) и по смерти Кида (seg000:1224);
- `pop_flip_screen()` в банке 8: `gfx_copy_page(VFLIP)` в скрытую страницу →
показать её → `gfx_copy_page(DIRECT)` во вторую → инвалидация слотов
персонажей, метки «фон трогали» и полосы HP;
- **чит-клавиша U** — это ПОРТ, а не костыль: в оригинале переворот тоже
висит на чит-клавише (seg000:0793). Без неё эффект проверяется только
честным проходом уровня 9 до комнаты 7 — чит-телепорт до зелья не
дотягивается (оно на `(1,7)`, а навигация ставит Кида на нижний ряд).
Цена: резидент +602 Б (новые функции libbgi линкуются в него), куча
1707 → 1050 Б. Это аргумент за [MEM-COLD2](../roomtest/TASKS_OPEN.md#mem-cold2)
до начала II.4.
### Контракт (исходное описание шага)
- `pop_upside` (uint8_t) — рядом с `pop_feather` в `pop_map.c`, экспорт в
`pop_map.h`;
- тоггл — в `pop_proc_get_object`, ветка `potion_type == 4` (сейчас там TODO,
как было у пера). Ничего кроме тоггла: ни вспышки, ни звука — так в
оригинале;
- сброс: `pop_start_level` (уже переехал в `roomtest_cold.c`) и смерть Кида —
по образцу seg000:1224 (в `kid_phys`/там, где ловится `alive >= 0`);
- переключение (функция `pop_flip_screen()`, банк 8 — холодный код):
1. `gfx_copy_rect(front, …, back, …, GFX_COPY_VFLIP)` — скрытая страница
получает перевёрнутый чистый фон (accel читает ОЗУ-копию, персонажей в
ней нет);
2. флип страниц, чтобы игрок сразу увидел результат;
3. `gfx_copy_rect(new_front, …, new_back, …, GFX_COPY_DIRECT)` — вторая
страница дабл-буфера получает то же;
4. инвалидация: `pop_cd[].valid = 0` для обоих слотов, `pop_mirror_heal`
-состояние, `pop_hp_invalidate()`, метки «фон трогали» (`pop_cd_touch`)
на обе страницы, `seam_sig` — чтобы шов перерисовался.
## II.2 Геометрия — единая точка пересчёта
```c
/* pop_bg.h рядом с POP_YOFF/POP_PLAYFIELD_H */
#define POP_FLIP_Y(y, h) (2 * POP_YOFF + POP_PLAYFIELD_H - (y) - (h))
```
Применяется ТОЛЬКО к тому, что внутри поля. Правило: любая функция,
получающая экранный `y`, либо сама зеркалит его по флагу, либо принимает уже
зеркальный — смешивать нельзя, иначе получим двойной переворот (это самый
вероятный класс багов здесь). Решение: зеркалит ВЫЗЫВАЮЩИЙ, у примитивов
семантика не меняется.
## II.3 Фон — СДЕЛАНО 2026-08-12
Флаг проведён в ЕДИНСТВЕННУЮ точку — `pop_blit_b`/`blit_b_clip`
(`pop_tile.c`): позиция пересчитывается макросом `FLIP_TOP` (он один на весь
слой — дублировать нельзя, иначе двойной переворот), а блит выбирает
vflip-двойник. Тем самым зеркалятся и полная отрисовка комнаты, и точечные
редрои, и анимации trob, и fore-слой, и кладка — все они ходят через эту
функцию.
Тонкость клипованного пути: при зеркале обрезка СВЕРХУ экрана съедает
НИЖНИЕ строки источника, поэтому кусок пересчитывается как
`sy' = h - sy - dh`.
Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком — пол
вверху, дверь уровня и кладка вверх ногами. Персонажи пока обычные (II.4).
Цена: три функции libbgi ушли в резидент, куча 1574 → 450 Б, поэтому тем же
заходом сделан **MEM-COLD2 п.1**`enter_room_side`/`enter_room` уехали в
банк 8 (куча вернулась к **1231 Б**, банк занят на 14.6%). Рабочие массивы
комнаты остались в `_DATA` и видны обеим половинам; `enter_room` обязана быть
`__banked` — её зовёт главный цикл.
### Контракт (исходное описание)
Одна точка: `pop_tile.c` (блит куска атласа) выбирает vflip-двойник по
`pop_upside` и пересчитывает `y`. Тогда автоматически попадают:
- полная отрисовка комнаты (`pop_room.c`);
- точечные редрои (`pop_redraw.c`: ворота, пики, кнопки, дверь уровня,
дрожащие плиты);
- анимации `pop_trob` (факелы, пузырьки зелий);
- fore-слой и оверлеи кромки, кладка стены (`pop_bg.c`);
- падающие куски loose (`pop_room.c`, `pop_loose_mob_*`).
Проверить отдельно: `wall_pattern` (кладка рисуется своей геометрией) и
оверлеи кромки — у них позиция считается от ряда, а не от `y` спрайта.
## II.4 Персонажи (column-major)
Нужны зеркальные кадры: 34 атласа, 228 563 Б (kid0..27 + sword, guard g0..g4).
- реверс колонки — через стек (`pop`/`push`), ~10 тактов номинала на байт;
- источник маппится в W0, приёмник — новая EMM-страница; на спрайт: прочитать
колонки в буфер W2 (максимальный кадр — 40×16 ≈ 640 Б), развернуть, писать
в приёмник (перемаппинг W0 на спрайт — один OUT);
- `pop_cdraw.c` выбирает набор атласов (оригинал/зеркальный) по `pop_upside`;
- **кэш ключуется по СТРАНИЦЕ атласа**, а не по кадру: страница = 8 кадров,
3-10 КБ, ~0.2-0.4 растрового кадра на переворот.
**Стратегия наполнения — ЛЕНИВЫЙ ПОСТРАНИЧНЫЙ** (решение пользователя
2026-08-12): страница переворачивается при первом обращении к ней в
перевёрнутом режиме. Реально в ходу 5-10 страниц из 34, и если игрок зелье не
трогал, не тратится ни такта, ни страницы.
**Время жизни кэша — ДО КОНЦА УРОВНЯ**: освобождаем страницы при смене
уровня, а не при снятии эффекта. На уровне 9 переворот случается минимум
дважды (второе зелье возвращает всё назад), плюс его снимает смерть Кида —
готовить пачку заново каждый раз было бы обидно.
После [UI-SPRITES](../roomtest/TASKS_OPEN.md#ui-sprites) страницы `kid27` и
`g0` станут чисто «полевыми» (деления HP уедут в константы резидента), и
исключений в кэше не останется — эту задачу логично сделать ДО II.4.
## II.5 Клип — ЧАСТЬ СДЕЛАНА 2026-08-12 (поле), остаётся clip_char
**Клип ПОЛЯ сделан.** Симптом (нашёл пользователь): после телепорта в
перевёрнутом виде ниже поля — мусор. Причина: спрайты, которые в обычном
виде торчат ВЫШЕ поля (полоса кладки у потолка, её режет `pop_t_clip_top`),
после отражения торчат НИЖЕ и лезут в борт, где живёт полоса HP.
Фикс в `blit_b_clip`: при перевороте режем по ОБЕИМ границам поля всегда, а
не по `pop_t_clip_top` — зеркальный спрайт может вылезти и там, где в
обычном виде клип не ставили. Плюс в `pop_blit_b` быстрый путь (без клипа)
теперь пропускает в клипующий всё, что выходит за поле.
Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
комнаты рисуются зеркально (факелы, чомперы, пол). То есть **переходы
между комнатами в перевёрнутом виде работают**.
Осталось из этого пункта: `clip_char` (верх поля ↔ низ) и клип падающей
плиты — они про ПЕРСОНАЖЕЙ и падающие объекты, то есть идут вместе с II.4.
### Контракт (исходное описание)
- `clip_char` (`pop_map.c:2287`, таблица `y_clip[5] = {-60,3,66,129,192}`) —
режет верх спрайта по линии ряда; при перевороте линия становится нижней.
Симметрия сама не сойдётся: нужен зеркальный расчёт `y_clip` и обмен
«сверху/снизу» местами;
- то же для зеркала уровня 4 (`pop_map.c:2732`) — на уровне 9 зеркал нет, но
код общий, поэтому ветку надо хотя бы не сломать;
- `pop_clip_sprite` (`pop_room.c:1069`) — клип падающей плиты;
- `pop_room_clip_borders` — борта симметричны (28 сверху и снизу), но
проверить, что чистится именно поле.
## II.6 Проверка
- **хост-тест** (`tests-host`): арифметика `POP_FLIP_Y` и зеркальный `y_clip`
— сцена «Кид у верхней кромки» в обычном и перевёрнутом режиме даёт
симметричные значения клипа. Логику отрисовки хост-тесты не видят, поэтому
здесь проверяем только счёт;
- **MAME**: уровень 9, комната 7 — выпить зелье `(1,7)`, проверить: картинка
перевернулась целиком, Кид ходит и цепляется корректно, факелы анимируются
перевёрнуто, ворота/пики перерисовываются перевёрнуто, полоса HP осталась
внизу и не зеркальная; второе зелье (комната 10) возвращает как было;
смерть Кида снимает эффект;
- **регресс**: обычные уровни (1-8) не должны измениться ни на пиксель —
прогнать пару комнат и сверить скриншоты с прежними.
---
# Часть III — ENTER-ROOM-FAST — СДЕЛАНО 2026-08-12
Комната теперь рисуется ОДИН раз — в скрытую страницу, — тут же
показывается флипом, а вторая страница получает её `gfx_copy_page(DIRECT)`.
Процесс рисования тайлов больше не виден: игрок получает готовый кадр.
Проверено в MAME: старт уровня и чит-переход между комнатами, два
последовательных снимка идентичны (обе страницы синхронны, мерцания нет).
Побочно понадобилось: видимую страницу главный цикл больше не ведёт сам, а
перечитывает у железа в начале кадра (`front = gfx_get_visible_page()`) —
её меняют и вход в комнату, и переворот, причём оба из банка, где локальные
переменные `main` недоступны.
# Часть III (исходное описание)
Идея пользователя, самостоятельная ценность (сейчас при входе в комнату видно,
как рисуются тайлы): рисовать комнату ОДИН раз в скрытую страницу, показать её
флипом, а во вторую страницу залить `gfx_copy_rect(…, GFX_COPY_DIRECT)`.
- сейчас `enter_room_side` рисует комнату дважды (цикл `for (pg = 0; pg < 2)`)
— вторая отрисовка (~полмиллиона тактов) меняется на копию (~115-200 К);
- процесс рисования перестаёт быть виден: игрок видит только готовый кадр;
- делается тем же `gfx_copy_rect`, что и I.3, то есть ничего дополнительно
писать не надо.
Проверять отдельно: состояние ОЗУ-копии второй страницы после копии
(инвариант из I.4 п.5), метки «фон трогали», и что heal на второй странице
работает.
---
# Порядок работ
1. I.0 разведка (`tests/accflip`) → факты в docs + memory.
2. I.1 ядро + I.3 `gfx_copy_rect` (+ `tests/pagecopy`).
3. II.1 состояние и `pop_flip_screen` — уже можно посмотреть в MAME:
картинка обязана перевернуться целиком, дальше она «поедет» по мере
перерисовок (это ожидаемо на этом шаге).
4. Часть III (ENTER-ROOM-FAST) — дешёвый выигрыш на том же кирпиче,
заодно обкатывает копию в бою.
5. I.2 vflip-блиты (+ `tests/blitvflip`) → II.3 фон. После этого сцена
должна выглядеть правильно во всём, кроме персонажей.
6. UI-SPRITES (деления HP в резидент) → II.4 зеркальные кадры персонажей.
7. II.5 клип → II.6 проверка и регресс.
Критерий готовности каждого шага — артефакт (побайтный тест в MAME либо
скриншот сцены), а не «код написан».
# Риски
- **Р1 не подтвердится** (нельзя менять Y между триггерами) — переворот
пойдёт через ОЗУ-буфер строки, бюджет вырастет вдвое (всё ещё ≈0.5
логического кадра), схема в целом выживает;
- **двойной переворот координат** — самый вероятный баг: часть кода зеркалит
`y` сама, часть получает уже зеркальный. Лечится правилом из II.2 и тем,
что зеркалит только вызывающий;
- **кэш зеркальных кадров и память**: 34 страницы EMM в худшем случае; по
`sprinter_emm_budget` на старте свободно 215 — влезает, но проверить
фактический остаток на уровне 9 (там уже загружены атласы фона, Кида,
стража и уровень);
- **fore-слой поверх персонажа**: он рисуется по футпринту спрайта, а
футпринт после переворота другой — проверить именно на сцене с колоннами.
# Решения пользователя (2026-08-12)
1. **Кэш зеркальных кадров — ленивый постраничный**, наполняется по первому
обращению (см. II.4).
2. **Живёт до конца уровня**, освобождается при смене уровня.
3. **ENTER-ROOM-FAST делается сразу**, шагом 4 — на готовом `gfx_copy_rect`,
до того как на копию завяжется переворот.
Открытых вопросов не осталось: план исполняется как есть, новые развилки —
только если разведка I.0 даст отрицательный результат по Р1.