План реализации L9-INVERT (docs/l9_invert_plan.md)
Разведка железа первым шагом (смена Port_Y между burst-триггерами и адресация двух страниц в W3), ядро копии с реверсом Y, vflip-блиты для row-major, gfx_copy_rect экран-экран; в приложении — состояние, единая точка пересчёта Y, фон через pop_tile.c, зеркальные кадры персонажей, клип. Побочной задачей ENTER-ROOM-FAST на том же кирпиче. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
# 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 РАЗВЕДКА ЖЕЛЕЗА — делать ПЕРВОЙ, до любого кода
|
||||
|
||||
Два факта, на которых стоит вся схема, сейчас НЕ подтверждены артефактом.
|
||||
Пока их нет, остальные пункты не начинать (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
|
||||
|
||||
Все 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 Состояние и момент переключения
|
||||
|
||||
- `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 Фон
|
||||
|
||||
Одна точка: `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 растрового кадра на переворот.
|
||||
|
||||
Стратегия наполнения кэша — см. открытый вопрос №1 внизу.
|
||||
|
||||
После [UI-SPRITES](../roomtest/TASKS_OPEN.md#ui-sprites) страницы `kid27` и
|
||||
`g0` станут чисто «полевыми» (деления HP уедут в константы резидента), и
|
||||
исключений в кэше не останется — эту задачу логично сделать ДО II.4.
|
||||
|
||||
## II.5 Клип
|
||||
|
||||
- `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
|
||||
|
||||
Идея пользователя, самостоятельная ценность (сейчас при входе в комнату видно,
|
||||
как рисуются тайлы): рисовать комнату ОДИН раз в скрытую страницу, показать её
|
||||
флипом, а во вторую страницу залить `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-слой поверх персонажа**: он рисуется по футпринту спрайта, а
|
||||
футпринт после переворота другой — проверить именно на сцене с колоннами.
|
||||
|
||||
# Открытые вопросы (ответы пользователя нужны до II.4)
|
||||
|
||||
1. Стратегия наполнения кэша зеркальных кадров: ленивый постраничный (по
|
||||
первому обращению), разом при входе на уровень с зельем типа 4, или офлайн
|
||||
в упаковщиках.
|
||||
2. Время жизни кэша: освобождать страницы при снятии эффекта / при выходе с
|
||||
уровня / держать до конца программы.
|
||||
3. ENTER-ROOM-FAST — делать сразу (шаг 4) или отложить до закрытия L9-INVERT.
|
||||
Reference in New Issue
Block a user