Порт 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>
21 KiB
План: консолидация работы с палитрами + переход уровня через 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. Зафиксированные решения
- Ступени затемнения: остаются 4. Вариант 8 ступеней той же сдвиговой техникой — рассмотреть отдельно, сейчас не внедрять.
- Предрасчёт fade-вариантов палитры отклонён. Аргументы: чтение файла с диска на порядок дороже вычисления; 3–7 КБ постоянной RAM при MEMORY=small непозволительны; предрасчёт привязан к конкретным палитрам, а снимок работает с любой текущей автоматически; keep_ui удвоил бы набор.
- Считать на лету, хранить один снимок (уже есть, бесплатно в хвосте страницы шрифта).
- Буферы на стеке, не статика (W1/W2 мало) и не 1 КБ: обнулить 64/256 байт дешевле, чем держать килобайт резидентно.
- Контракт
gfx_pal_load(pal, start, count, data): count — число СЛОТОВ, буфер обязан бытьcount*4байт; count=0 означает «все 256». - Leaf-applеры остаются на месте (
pop_bg_pal_apply— банк 7 со своими таблицами,pop_shadow_pal_apply,pop_guard_set_palette): банковая rodata чужого банка не видна, перенос сломал бы доступ к данным. - Молния (
flash_bgв sprpop.c) не переносится — игровой эффект записи 0; после вспышки восстановление записи 0 из снимка ложится на API. - Модель состояния: разделены «какая палитра логически загружена» (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)
- Создан
src/pop_pal.c/.hв bank 10, добавлен в Makefile. Он владеет политикойload logical palette → snapshot → apply brightness. Низкоуровневые snapshot/dim/fade остаются физически вpop_ui.c: там владелец страницы FONT.ATL, где лежит снимок; наружу они доступны только черезpop_pal. - Заменены call-sites:
sprpop_cold.c~958: black → game_load вместо тройки;pop_title.ctitle_restore_game_palette → game_load; загрузка title.pal → title_load;pop_intro.cintro_load/intro_restore → story_load/game_load;pop_hof.c(2 × story.pal) → story_load;pop_menu.c: fade/dim → новые имена (dim с keep_ui — низкоуровневый экспорт);sprpop.cdemo-start (snapshot+dim(4,0)+fade_in(4)) → новый API.
- Старые вызовы не остаются в коде приложения; внутренние функции
pop_uiсохранены как реализации одного владельца памяти снимка. - Сборка и host-тесты пройдены.
make size-checkнеприменим: меняется приложение, а не libc/libbgi. - 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_pending2026-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 (зафиксирована атласами).