31b82661eb
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
298 lines
21 KiB
Markdown
298 lines
21 KiB
Markdown
# План: консолидация работы с палитрами + переход уровня через fade
|
||
|
||
Статус: **этапы A и B реализованы; визуальная приёмка полного маршрута ещё
|
||
идёт** (2026-08-24). Палитры выделены в bank 10, а renderer cutscene/intro —
|
||
в bank 11, чтобы не переполнять bank 9 оболочки.
|
||
Обсуждение велось вокруг `SprPoP/` (банк 9 — оболочка, fade из `pop_ui.c`).
|
||
|
||
---
|
||
|
||
## 1. Текущее состояние: карта палитры
|
||
|
||
Палитра Sprinter — 256 записей по 4 байта (B, G, R, 0) = 1 КБ на страницу.
|
||
У страниц дабл-буфера ДВЕ раздельные палитры (`gfx_pal_load(0,…)` и
|
||
`gfx_pal_load(1,…)` — почти всегда парой). BIOS читает буферы только из
|
||
#4000–#BFFF: банковую rodata напрямую отдавать нельзя (копия в стек/W2),
|
||
см. грабли `pop_guard_set_palette` и `bg_load_tile_pal`.
|
||
|
||
### 1.1 Игровая палитра `KID\kid.pal` — раскладка слотов
|
||
|
||
Собирается `toolchain/pop_pack_kid.py build_palette()`, грузится одним
|
||
`gfx_pal_fload` (перезаписывает все 256 записей). Атласы запекались под эти
|
||
индексы — менять раскладку нельзя без перепаковки ассетов.
|
||
|
||
| Слоты | Назначение | Источник | Динамика |
|
||
|---|---|---|---|
|
||
| 0x00 | Цвет фона + **вспышка молнии** (подмена записи 0, `flash_bg` ← do_flash/set_bg_attr SDLPoP) | — | меняется в игре |
|
||
| 0x01–0x2F | Не закреплены (нули) | — | свободно |
|
||
| 0x30–0x3F | VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) | `VGA16[]` | статично |
|
||
| ↳ 0x37–0x3F | Поддиапазон **UI**: текст/рамка меню; единственное, что `keep_ui` не затемняет (`MENU_BORDER`=0x37) | — | — |
|
||
| 0x40–0x4F | chtab_1 пламя/зелья (`POT_PAL_BASE`) | VDUNGEON res150.pal | статично |
|
||
| 0x50–0x5F | **ENV фон тайлсета** (`POP_PAL_ENV`) | res200.pal набора | **меняется при смене тайлсета** |
|
||
| 0x60–0x6F | **WALL тайлсета** (`POP_PAL_WALL`) | res360.pal набора | **меняется при смене тайлсета** |
|
||
| 0x70–0x7F | Kid (`PAL_BASE`) | KID res400.pal | статично |
|
||
| 0x80–0x8F | Меч chtab_0 (`SWORD_PAL_BASE`) | POT res700.pal | статично |
|
||
| 0x90–0x9F | Страж chtab_5 (`GUARD_PAL_BASE`) | res10.bin guard_palettes | **меняется по КОМНАТАМ** |
|
||
| 0xA0–0xAF | Тень (`POP_SHADOW_PAL_BASE`) | RGB-сетка pop_pack_shadow.py | статично |
|
||
| 0xB0–0xFF | Свободны (5 слотов) | — | — |
|
||
|
||
Итого динамических зон три: запись 0 (молния), env+wall (тип здания),
|
||
стражи (per-room). Всё остальное одинаково всю игру.
|
||
|
||
### 1.2 Полноэкранные палитры заставок
|
||
|
||
Каждая перезаписывает ВСЕ 256 записей:
|
||
|
||
| Файл | Где используется |
|
||
|---|---|
|
||
| `KID\kid.pal` (+ fallback `a:\kid.pal`) | BOOT и возврат в игру после заставок |
|
||
| `TITLE\title.pal` | экран TITLE |
|
||
| `PV\story.pal` | INTRO и HALL_OF_FAME (одна палитра на обе фазы) |
|
||
|
||
### 1.3 Тайлсеты: подземелье ↔ дворец
|
||
|
||
Оба набора используют ОДНИ И ТЕ ЖЕ слоты 0x50–0x5F/0x60–0x6F, заполняя их
|
||
разными цветами (атласы обоих наборов запекались под эти индексы).
|
||
Переключение = загрузка 64 байт (32 записи env+wall) в обе страницы
|
||
(`bg_load_tile_pal`); остальные 224 записи не трогаются.
|
||
|
||
Какие уровни дворец — `tbl_level_type` (`pop_level_cold.c:44`):
|
||
**4, 5, 6, 10, 11, 14**; остальные подземелье.
|
||
|
||
Палитра дворца `pal_tile.pal` (расшифровка, формат записи B,G,R):
|
||
|
||
ENV 0x50–0x5F (пол, ковры, факелы, ворота, пики, арки):
|
||
|
||
| Слот | RGB | | Слот | RGB |
|
||
|---|---|---|---|---|
|
||
| 50 | 0,0,0 чёрный | | 58 | 202,190,178 серо-бежевый |
|
||
| 51 | 121,89,60 коричневый | | 59 | 153,133,129 серо-лиловый |
|
||
| 52 | 161,121,76 светло-коричневый | | 5A | 76,64,56 тёмный серо-бурый |
|
||
| 53 | 194,149,89 песочный | | 5B | 153,97,89 кирпично-красный |
|
||
| 54 | 230,178,113 яркий песок | | 5C | 137,80,72 тёмный кирпич |
|
||
| 55 | 246,202,125 кремовый | | 5D | 48,125,125 бирюзовый |
|
||
| 56 | 255,234,170 бледно-кремовый | | 5E | 12,56,89 тёмно-синий |
|
||
| 57 | 255,255,255 белый | | 5F | 202,56,28 красно-оранжевый |
|
||
|
||
WALL 0x60–0x6F (вся палитра песочная): 61=(218,170,89), 62=(226,165,93),
|
||
63=(226,170,97), 64=(218,161,85), 65=белый, 66=(226,165,93), 67=(218,165,89),
|
||
68=(226,170,89), 69=(218,170,97), 6A=(255,210,137), 6B=(255,218,149),
|
||
6C=(255,210,137), 6D=(255,218,145), 6E=(194,153,80 тёмный песок),
|
||
6F=(238,186,117).
|
||
|
||
Чем рисуется во дворце:
|
||
- **Тело стены — НЕ спрайты**, а сплошные заливки; цвет разыгрывается на
|
||
комнату prandom'ом (`gen_palace_wall_colors`, `pop_bg.c:140`, порт
|
||
seg000:1942): подряды 1 и 3 берут случайный из 0x61–0x64, подряды 0 и 2 —
|
||
из 0x66–0x69; соседи по горизонтали не повторяются.
|
||
- Декор стен id 3–17 — mono-силуэт цветом VGA16+6 (0x36).
|
||
- Верх дверных проёмов дворца — спец-id 78–84 + полоса 145 («полоса под
|
||
окнами», pop_room.c:478).
|
||
- Остальное (пол, ковры, порталы-факелы, ворота, пики) — env-куски
|
||
pal_env*.atl с ENV-таблицей выше.
|
||
|
||
### 1.4 Стражи (0x90–0x9F)
|
||
|
||
Цвет задаётся на КОМНАТУ (`level.guards_color[room-1]`), при входе в
|
||
комнату зовётся `pop_guard_set_palette(color)` ДО отрисовки (слоты общие
|
||
на экран — смена посреди кадра дала бы стража в новой палитре с полосой HP
|
||
в старой). Только для обычных стражей (`tbl_guard_type == 0`): скелет и
|
||
Джафар имеют собственную палитру, зашитую в kid.pal; им зовётся с color=0
|
||
(не трогать — иначе Джафар на ур.13 покрасился бы в цвет стража своей
|
||
комнаты). Внутри одного уровня слоты могут перезаписываться многократно.
|
||
|
||
## 2. Текущее состояние: механика fade
|
||
|
||
### 2.1 Наша реализация (`pop_ui.c`, банк 9)
|
||
|
||
- `pop_ui_palette_snapshot()` — снимок всех 256 записей через
|
||
`gfx_pal_get` по 4 чанкам × 64; хранится в хвосте страницы шрифта
|
||
FONT.ATL ([0x3C00,0x4000)), map/unmap W0. Требует `font_ready`.
|
||
- `pop_ui_palette_dim(step, keep_ui)` — готовит ОБЕ экранные палитры из
|
||
снимка. Шкала без умножений (только сдвиги):
|
||
|
||
| Шаг | Формула на канал | Яркость |
|
||
|---|---|---|
|
||
| 0 | x | оригинал |
|
||
| 1 | `(x>>1)+(x>>2)` | ≈3/4 |
|
||
| 2 | `x>>1` | 1/2 |
|
||
| 3 | `x>>2` | 1/4 |
|
||
| 4 | 0 | чёрный |
|
||
|
||
`keep_ui` пропускает 0x37–0x3F (меню остаётся ярким).
|
||
- `pop_ui_fade_out/in(steps)` — проигрывание ступеней за `steps` кадров
|
||
vsync (`step = i*4/steps`, целочисленно): steps=4 — канонический (по кадру
|
||
на ступень), steps<4 — перескакивает ступени, steps>4 — повторяет (плавнее),
|
||
steps=0 у fade_in — мгновенный restore.
|
||
- Контракт map/unmap: обращения к EMM/W0 и BIOS-палитре строго после unmap.
|
||
|
||
Стоимость одного dim ≈ 15–25 тыс. тактов (~4–7 мс при 3.5 МГц) —
|
||
укладывается в кадр vsync, на практике лагов нет.
|
||
|
||
### 2.2 Как сделано в SDLPoP (seg009.c, USE_FADE/gmMcgaVga)
|
||
|
||
- fade_out: каждый кадр КАЖДЫЙ ненулевой канал каждой записи −1; до нуля.
|
||
- fade_in: `fade_pos` от 0x40 вниз; канал +1, пока меньше оригинала.
|
||
- Уровней затемнения до 63–64 (VGA-канал 6 бит), полный фейд ~63 кадра ×
|
||
wait_time=2 тика — медленно и кинематографично.
|
||
- `which_rows` — битовая маска групп по 16 записей: можно фейдить часть
|
||
палитры (в оригинале используется).
|
||
- По завершении принудительно восстанавливается оригинал; после out экран
|
||
заливается чёрным.
|
||
|
||
Это осознанное расхождение (скорость/такты vs плавность) — ЗАПИСАТЬ в
|
||
`docs/impl_diff.md` (сейчас записи нет).
|
||
|
||
## 3. Зафиксированные решения
|
||
|
||
1. **Ступени затемнения: остаются 4.** Вариант 8 ступеней той же сдвиговой
|
||
техникой — рассмотреть отдельно, сейчас не внедрять.
|
||
2. **Предрасчёт fade-вариантов палитры отклонён.** Аргументы: чтение файла
|
||
с диска на порядок дороже вычисления; 3–7 КБ постоянной RAM при
|
||
MEMORY=small непозволительны; предрасчёт привязан к конкретным палитрам,
|
||
а снимок работает с любой текущей автоматически; keep_ui удвоил бы набор.
|
||
3. **Считать на лету**, хранить один снимок (уже есть, бесплатно в хвосте
|
||
страницы шрифта).
|
||
4. **Буферы на стеке**, не статика (W1/W2 мало) и не 1 КБ: обнулить 64/256
|
||
байт дешевле, чем держать килобайт резидентно.
|
||
5. **Контракт `gfx_pal_load(pal, start, count, data)`**: count — число
|
||
СЛОТОВ, буфер обязан быть `count*4` байт; count=0 означает «все 256».
|
||
6. **Leaf-applеры остаются на месте** (`pop_bg_pal_apply` — банк 7 со своими
|
||
таблицами, `pop_shadow_pal_apply`, `pop_guard_set_palette`): банковая
|
||
rodata чужого банка не видна, перенос сломал бы доступ к данным.
|
||
7. **Молния (`flash_bg` в sprpop.c) не переносится** — игровой эффект
|
||
записи 0; после вспышки восстановление записи 0 из снимка ложится на API.
|
||
8. Модель состояния: разделены «какая палитра логически загружена» (load_*)
|
||
и «с какой яркостью показана» (apply/fade). Любой load_* обновляет снимок;
|
||
apply/fade показывает его с нужной глубиной. Это позволяет грузить новую
|
||
палитру «в темноте» (экран остаётся чёрным, пока не позвали apply/fade_in).
|
||
|
||
## 4. Целевой API `pop_pal.c/.h` (банк 9)
|
||
|
||
```c
|
||
/* сброс */
|
||
void pop_pal_black(void) __banked;
|
||
/* все 256 записей ОБЕИХ страниц = 0. Стековый buf[256], обнуление циклом,
|
||
* 8 вызовов gfx_pal_load (4 чанка × 2 страницы, паттерн как в dim).
|
||
* Зовётся СРАЗУ ПОСЛЕ initgraph в pop_boot (раньше нельзя — нет гарантий
|
||
* состояния графического режима): закрывает кейс «мусор/палитра предыдущей
|
||
* программы при включении графики». СНИМОК НЕ ТРОГАЕТ (контракт:
|
||
* чёрный экран без изменения логической палитры). */
|
||
|
||
/* загрузка (пишет полную палитру в обе страницы + refresh снимка;
|
||
* видимую яркость НЕ трогают — экран меняется только по apply/fade) */
|
||
void pop_pal_file_load(const char *name) __banked;
|
||
/* gfx_pal_fload + fallback "a:\" + gfx_pal_sync (fallback сегодня
|
||
* скопирован в каждом из ~6 мест вызова) */
|
||
|
||
void pop_pal_game_load(void) __banked;
|
||
/* file_load("KID\kid.pal") + pop_bg_pal_apply + pop_shadow_pal_apply.
|
||
* Сегодня тройка скопирована 3 раза (sprpop_cold ~958, pop_title ~88,
|
||
* pop_intro ~183). Единое место инварианта «kid.pal затирает слоты
|
||
* тайлсета 0x50..0x6F и тени 0xA0..0xAF». */
|
||
|
||
void pop_pal_level_load(uint8_t full) __banked;
|
||
/* палитра уровня: kid.pal/shadow + tileset 0x50..0x6F если набор сменился
|
||
* (сравнение через pop_level_type()). full=1 — ПРИНУДИТЕЛЬНО перечитать
|
||
* kid.pal/shadow (один экспорт с флагом, не две функции — меньше банковых
|
||
* точек входа). СТРАЖЕЙ (0x90..0x9F) НЕ включает: это компетенция входа
|
||
* в комнату (pop_guard_set_palette до первого draw). */
|
||
|
||
void pop_pal_story_load(void) __banked; /* PV\story.pal (INTRO и HOF — файл один, функция одна) */
|
||
void pop_pal_title_load(void) __banked; /* TITLE\title.pal */
|
||
|
||
/* отображение */
|
||
void pop_pal_snapshot(void) __banked; /* переезд из pop_ui, тело то же */
|
||
void pop_pal_apply(uint8_t fade) __banked; /* = dim(fade, 0), 0..4 */
|
||
void pop_pal_fade_in(uint8_t steps) __banked; /* переезд из pop_ui */
|
||
void pop_pal_fade_out(uint8_t steps) __banked;
|
||
|
||
/* меню продолжает звать низкоуровневый dim(step, keep_ui=1) — отдельный
|
||
* тонкий экспорт, чтобы не тащить флаг в горячий apply. Старые имена
|
||
* pop_ui_palette_* / pop_ui_fade_* УДАЛЯЮТСЯ (без алиасов — меньше
|
||
* экспорта банка). */
|
||
```
|
||
|
||
Соответствие старое→новое: snapshot→snapshot, restore→apply(0),
|
||
fade_out/in→fade_out/in, тройка kid.pal×3→game_load, fload+fallback+sync×6→file_load.
|
||
|
||
## 5. Этап A: рефакторинг — выполнен (2026-08-24)
|
||
|
||
1. Создан `src/pop_pal.c/.h` в **bank 10**, добавлен в Makefile.
|
||
Он владеет политикой `load logical palette → snapshot → apply brightness`.
|
||
Низкоуровневые snapshot/dim/fade остаются физически в `pop_ui.c`: там
|
||
владелец страницы FONT.ATL, где лежит снимок; наружу они доступны только
|
||
через `pop_pal`.
|
||
2. Заменены call-sites:
|
||
- `sprpop_cold.c` ~958: black → game_load вместо тройки;
|
||
- `pop_title.c` title_restore_game_palette → game_load; загрузка title.pal → title_load;
|
||
- `pop_intro.c` intro_load/intro_restore → story_load/game_load;
|
||
- `pop_hof.c` (2 × story.pal) → story_load;
|
||
- `pop_menu.c`: fade/dim → новые имена (dim с keep_ui — низкоуровневый экспорт);
|
||
- `sprpop.c` demo-start (snapshot+dim(4,0)+fade_in(4)) → новый API.
|
||
3. Старые вызовы не остаются в коде приложения; внутренние функции `pop_ui`
|
||
сохранены как реализации одного владельца памяти снимка.
|
||
4. Сборка и host-тесты пройдены. `make size-check` неприменим: меняется
|
||
приложение, а не libc/libbgi.
|
||
5. MAME smoke-тест полного цикла смен палитр: boot → title (title.pal +
|
||
fade) → intro (story/kid) → demo fade-in → игра → HOF (story.pal).
|
||
Проверить: отсутствие мусора при включении графики (эффект black),
|
||
меню с keep_ui остаётся ярким при затемнении, молния (запись 0)
|
||
восстанавливается.
|
||
|
||
## 6. Этап B: переход уровня через fade — реализован, ждёт визуальной приёмки
|
||
|
||
Сценарий (обсуждён, детали уточнить по SDLPoP перед реализацией — как
|
||
оригинал делает смену уровня, есть ли там fade в DOS-версии):
|
||
|
||
```
|
||
fade_out // последний кадр уровня N темнеет
|
||
рисуем комнату 1 уровня N+1 // во ВТОРУЮ страницу, в темноте
|
||
pop_pal_level_load(full=0) // новая палитра: железо+снимок обновлены,
|
||
// экран всё ещё чёрный
|
||
флип + копия второй страницы обратно в первую
|
||
fade_in // = анимированный apply 3→2→1→0
|
||
```
|
||
|
||
Экономия: реально переезжают только 32 записи (env/wall) при смене набора
|
||
dungeon↔palace; guards_color обновит вход в комнату. Kid/shadow не меняются
|
||
— потому full=0.
|
||
|
||
Реализация находится в `sprpop.c` / `sprpop_cold.c`: последний кадр
|
||
уровня N темнеет, `pop_level_switch()` подготавливает первый кадр N+1 и
|
||
обновляет логический источник через `pop_pal_level_load(1)`, затем главный
|
||
цикл показывает кадр только через fade-in. Восемь ступеней и отдельная
|
||
анимация смерти не входят в этот этап.
|
||
|
||
**Этап B закрывает два открытых бага** (разборы — `BUGS_OPEN.md`):
|
||
- [PAL-L1-AFTER-INTRO] — вход в игру на уровень 1 после интро с чёрным
|
||
экраном (маршрут demo_new_game; корень не установлен, воспроизведение
|
||
нестабильно);
|
||
- [PAL-DUNGEON-STALE] — переход 3→4 оставляет подземную палитру (корень
|
||
ясен: fade_in восстанавливает из снимка, снятого ДО загрузки тайлсета
|
||
дворца; быстрый фикс `fade_in_pending` 2026-08-23 сам же и проявляет этот
|
||
дефект модели).
|
||
|
||
Быстрый фикс 2026-08-23 (маршрут CUTSCENE → LEVEL_LOAD → PLAYING,
|
||
`fade_in_pending` + `pop_ui_fade_in(4)` после `pop_level_switch`) закрыл
|
||
чёрный экран на переходах с pre-cutscene внутри подземелья (1→2), но модель
|
||
«кто и когда меняет яркость» остаётся разношёрстной — её и приводит в
|
||
порядок этап B.
|
||
|
||
## 7. Этап C: документирование
|
||
|
||
- Запись в `docs/impl_diff.md`: наши 4 ступени vs SDLPoP ~64 (что делает
|
||
оригинал, что делаем мы — сдвиговая шкала ради тактов, чем платим —
|
||
грубее градации, что проверять при регрессе).
|
||
- После этапа B — дополнить запись про сам переход.
|
||
|
||
## 8. Не трогаем
|
||
|
||
- Молнию (`flash_bg`, sprpop.c) — включая обход SDCC-бага
|
||
`gfx_pal_set(0,0,0,0,0)` → ручные `gfx_pal_set(0/1, 0, r,g,b)`;
|
||
- leaf-applеры: `pop_bg_pal_apply` (банк 7), `pop_shadow_pal_apply`,
|
||
`pop_guard_set_palette` (данные своих модулей);
|
||
- хранилище снимка в хвосте страницы шрифта FONT.ATL (бесплатное место,
|
||
guard `font_ready`);
|
||
- раскладку слотов 0x00–0xAF (зафиксирована атласами).
|