Files
Sprinter-SDCC/applications/SprPoP/docs/palette_plan.md
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт 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>
2026-08-27 12:12:28 +03:00

298 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: консолидация работы с палитрами + переход уровня через 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 | Не закреплены (нули) | — | свободно |
| 0x300x3F | VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) | `VGA16[]` | статично |
| ↳ 0x37–0x3F | Поддиапазон **UI**: текст/рамка меню; единственное, что `keep_ui` не затемняет (`MENU_BORDER`=0x37) | — | — |
| 0x400x4F | chtab_1 пламя/зелья (`POT_PAL_BASE`) | VDUNGEON res150.pal | статично |
| 0x500x5F | **ENV фон тайлсета** (`POP_PAL_ENV`) | res200.pal набора | **меняется при смене тайлсета** |
| 0x600x6F | **WALL тайлсета** (`POP_PAL_WALL`) | res360.pal набора | **меняется при смене тайлсета** |
| 0x700x7F | Kid (`PAL_BASE`) | KID res400.pal | статично |
| 0x800x8F | Меч chtab_0 (`SWORD_PAL_BASE`) | POT res700.pal | статично |
| 0x900x9F | Страж chtab_5 (`GUARD_PAL_BASE`) | res10.bin guard_palettes | **меняется по КОМНАТАМ** |
| 0xA00xAF | Тень (`POP_SHADOW_PAL_BASE`) | RGB-сетка pop_pack_shadow.py | статично |
| 0xB00xFF | Свободны (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 Тайлсеты: подземелье ↔ дворец
Оба набора используют ОДНИ И ТЕ ЖЕ слоты 0x500x5F/0x600x6F, заполняя их
разными цветами (атласы обоих наборов запекались под эти индексы).
Переключение = загрузка 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 0x500x5F (пол, ковры, факелы, ворота, пики, арки):
| Слот | 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 0x600x6F (вся палитра песочная): 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 Стражи (0x900x9F)
Цвет задаётся на КОМНАТУ (`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 (зафиксирована атласами).