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

21 KiB
Raw Blame History

План: консолидация работы с палитрами + переход уровня через 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) меняется в игре
0x010x2F Не закреплены (нули) свободно
0x300x3F VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) VGA16[] статично
↳ 0x370x3F Поддиапазон 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)

/* сброс */
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 (зафиксирована атласами).