План реализации 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.
|
||||||
@@ -50,6 +50,10 @@
|
|||||||
<a id="l9-invert"></a>
|
<a id="l9-invert"></a>
|
||||||
### L9-INVERT. Зелье ПЕРЕВОРОТА (уровень 9, комнаты 7 и 10)
|
### L9-INVERT. Зелье ПЕРЕВОРОТА (уровень 9, комнаты 7 и 10)
|
||||||
|
|
||||||
|
> **Подробный план реализации — [`../docs/l9_invert_plan.md`](../docs/l9_invert_plan.md)**
|
||||||
|
> (задачи libbgi, задачи приложения, порядок работ, риски, открытые вопросы).
|
||||||
|
> Ниже — механика и обоснование решений; план исполняется по документу.
|
||||||
|
|
||||||
Единственное новое на уровнях 9-11: зелья типа 4 на `(1,7)` комнаты 7 и
|
Единственное новое на уровнях 9-11: зелья типа 4 на `(1,7)` комнаты 7 и
|
||||||
`(0,4)` комнаты 10 (сверено по `res2009.bin`). Уровни 10 и 11 не приносят ни
|
`(0,4)` комнаты 10 (сверено по `res2009.bin`). Уровни 10 и 11 не приносят ни
|
||||||
тайлов, ни спецсобытий — только дворцовый тайлсет (есть) и стражи skill 3-4.
|
тайлов, ни спецсобытий — только дворцовый тайлсет (есть) и стражи skill 3-4.
|
||||||
|
|||||||
Reference in New Issue
Block a user