# План: консолидация работы с палитрами + переход уровня через fade Статус: **этапы A и B реализованы; визуальная приёмка полного маршрута ещё идёт** (2026-08-24). Палитры выделены в bank 10, а renderer cutscene/intro — в bank 11, чтобы не переполнять bank 9 оболочки. Обсуждение велось вокруг `roomtest/` (банк 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` в roomtest.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 раза (roomtest_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. Создан `roomtest/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: - `roomtest_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 — низкоуровневый экспорт); - `roomtest.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. Реализация находится в `roomtest.c` / `roomtest_cold.c`: последний кадр уровня N темнеет, `pop_level_switch()` подготавливает первый кадр N+1 и обновляет логический источник через `pop_pal_level_load(1)`, затем главный цикл показывает кадр только через fade-in. Восемь ступеней и отдельная анимация смерти не входят в этот этап. **Этап B закрывает два открытых бага** (разборы — `roomtest/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`, roomtest.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 (зафиксирована атласами).