f541c0aad9
Найдено пользователем: после телепорта в перевёрнутом виде ниже поля мусор. Спрайты, торчащие в обычном виде ВЫШЕ поля (полоса кладки у потолка), после отражения торчат НИЖЕ и лезут в борт с полосой HP. blit_b_clip при перевороте режет по обеим границам поля всегда, а не по pop_t_clip_top; быстрый путь pop_blit_b отдаёт клипующему всё, что выходит за поле. Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые, комнаты (факелы, чомперы, пол) зеркальны. Переходы между комнатами в перевёрнутом виде работают. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
429 lines
31 KiB
Markdown
429 lines
31 KiB
Markdown
# 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.
|