Разведка железа первым шагом (смена 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>
21 KiB
L9-INVERT — план реализации зелья переворота (уровень 9)
Рабочий план, по которому задача делается с чистого контекста. Доска —
../roomtest/TASKS_OPEN.md#l9-invert; правила
подпроекта — ../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 Копия прямоугольника экран→экран
#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 Чего в списке пользователя не хватало
- Размерный регресс: после каждого шага
make size-check; новые модули не должны утянуть за собой рост в программах, которые их не зовут (проверка наexamples/иtests/). - Обе сборки библиотеки — fast и safe (
sprinter-cc --safe): гарды параметров в safe, критичные — в обеих. - Документация:
docs/sprite-api-design.md(раздел про блиттер),docs/TODO.md, справочник API; факты разведки I.0 — вdocs/new/06-accel.md. - IM2/CBL: длинные серии коротких DI-окон не должны ломать кадровые
прерывания и клавиатуру — проверить
tests/kbdpoll-сценарием после внедрения (клавиатура вычерпывается idle-хуком, а переворот идёт вне ожидания кадра). - Порядок «страница ↔ ОЗУ-копия»: копия должна идти банком
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 — холодный код):gfx_copy_rect(front, …, back, …, GFX_COPY_VFLIP)— скрытая страница получает перевёрнутый чистый фон (accel читает ОЗУ-копию, персонажей в ней нет);- флип страниц, чтобы игрок сразу увидел результат;
gfx_copy_rect(new_front, …, new_back, …, GFX_COPY_DIRECT)— вторая страница дабл-буфера получает то же;- инвалидация:
pop_cd[].valid = 0для обоих слотов,pop_mirror_heal-состояние,pop_hp_invalidate(), метки «фон трогали» (pop_cd_touch) на обе страницы,seam_sig— чтобы шов перерисовался.
II.2 Геометрия — единая точка пересчёта
/* 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 страницы 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 на второй странице работает.
Порядок работ
- I.0 разведка (
tests/accflip) → факты в docs + memory. - I.1 ядро + I.3
gfx_copy_rect(+tests/pagecopy). - II.1 состояние и
pop_flip_screen— уже можно посмотреть в MAME: картинка обязана перевернуться целиком, дальше она «поедет» по мере перерисовок (это ожидаемо на этом шаге). - Часть III (ENTER-ROOM-FAST) — дешёвый выигрыш на том же кирпиче, заодно обкатывает копию в бою.
- I.2 vflip-блиты (+
tests/blitvflip) → II.3 фон. После этого сцена должна выглядеть правильно во всём, кроме персонажей. - UI-SPRITES (деления HP в резидент) → II.4 зеркальные кадры персонажей.
- II.5 клип → II.6 проверка и регресс.
Критерий готовности каждого шага — артефакт (побайтный тест в MAME либо скриншот сцены), а не «код написан».
Риски
- Р1 не подтвердится (нельзя менять Y между триггерами) — переворот пойдёт через ОЗУ-буфер строки, бюджет вырастет вдвое (всё ещё ≈0.5 логического кадра), схема в целом выживает;
- двойной переворот координат — самый вероятный баг: часть кода зеркалит
yсама, часть получает уже зеркальный. Лечится правилом из II.2 и тем, что зеркалит только вызывающий; - кэш зеркальных кадров и память: 34 страницы EMM в худшем случае; по
sprinter_emm_budgetна старте свободно 215 — влезает, но проверить фактический остаток на уровне 9 (там уже загружены атласы фона, Кида, стража и уровень); - fore-слой поверх персонажа: он рисуется по футпринту спрайта, а футпринт после переворота другой — проверить именно на сцене с колоннами.
Открытые вопросы (ответы пользователя нужны до II.4)
- Стратегия наполнения кэша зеркальных кадров: ленивый постраничный (по первому обращению), разом при входе на уровень с зельем типа 4, или офлайн в упаковщиках.
- Время жизни кэша: освобождать страницы при снятии эффекта / при выходе с уровня / держать до конца программы.
- ENTER-ROOM-FAST — делать сразу (шаг 4) или отложить до закрытия L9-INVERT.