From 7377bf6a2ce38d886b0c5928798a3883205129c0 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 2 Sep 2026 11:30:53 +0300 Subject: [PATCH] =?UTF-8?q?SprPoP:=20=D0=B7=D0=B0=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=20=D0=BC=D1=83=D0=B7=D1=8B=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=81=D1=80=D0=B0=D0=B7=D1=83=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=81=D0=B5=D0=BA?= =?UTF-8?q?=D1=83=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit От запуска до проявления титула проходило ~3 с чёрного экрана в тишине: pop_boot читал ВСЕ ресурсы игры до первого пикселя, а pop_title_show перед fade_in вычитывал целиком трек заставки (248 КБ). Старт разбит на три фазы (sprpop_cold.c): - pop_boot() — настройки, POP.CFG, графика, чёрный экран, FONT.ATL; - pop_boot_sound() — snd.idx + ОДНА страница snd.arc, индекс музыки, CBL; - pop_boot_rest() — остальные страницы звука, kid.ani, Тень, атласы Кида; идемпотентно, между кусками доливает трек. pop_title_show теперь: fade_in -> pop_boot_sound -> pop_music_stream(54) -> pop_boot_rest под стоящим титулом. Порядок «сначала fade_in, потом тема» сохранён как у show_title (seg000.c:1981). Первый такт заставки укорочен на фактически потраченное время (часы насоса 85,4 Гц -> кадры луча сдвигами), иначе сцена уехала бы относительно музыки. title_wait доливает трек по полстраницы за кадр. Музыка играет с ПЕРВОЙ страницы: pop_music_stream уже был (игровые джинглы), кольцо won не понадобилось — m54 это 16 страниц из 20 доступных. Звук поднимается в два приёма (pop_sfx_init_begin/finish): насосу для тишины нужен ровно один блок, и упаковщик обязан класть его первым блоком страницы 0 (tools/pop_idx.py) — значит для открытия CBL хватает ОДНОЙ страницы (33 мс вместо 440). Пока набор неполон, pop_sfx_play пропускает эффекты; заявки на музыку проходят. Четыре загрузки из boot УБРАНЫ, а не отложены: атлас фона, страж, уровень 1 и pop_pal_game_load — их и так делает pop_level_switch на входе в любой уровень (а pop_new_game_load ещё и палитру перед ним), и оба маршрута в игру, LEVEL_LOAD и DEMO, упираются в него. Это ~0,77 с на ровном месте. Так же устроен оригинал: init_game_main до заставки читает только меч, пламя, звуки и палитры, а chtab_5/6/7 грузит load_lev_spr на старте уровня. Страховка на случай пропуска заставки в первую секунду — идемпотентный pop_boot_rest() в начале pop_new_game_load и pop_level_switch. FONT.ATL остаётся в минимальной фазе, и это не про шрифт: снимок палитры физически лежит в хвосте его страницы (pop_ui.c), а вся машинерия яркости начинается с `if (!font_ready) return`. С отложенным шрифтом заставка возникала разом на полной яркости — поймано пользователем. Проверено в MAME: титул виден сразу и проявляется полосами, pop_mus_left убывает (трек стримится), pop_snd_pages=10 (набор дочитался), цепочка титул -> интро -> пропуск -> уровень 1 работает. Банк 8: 95,5 % (732 Б). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4 --- applications/SprPoP/src/pop_sfx.c | 7 + applications/SprPoP/src/pop_sfx.h | 26 +++- applications/SprPoP/src/pop_sfx_cold.c | 67 ++++++++-- applications/SprPoP/src/pop_title.c | 54 ++++++-- applications/SprPoP/src/sprpop_cold.c | 169 +++++++++++++++++-------- applications/SprPoP/src/sprpop_cold.h | 16 ++- 6 files changed, 259 insertions(+), 80 deletions(-) diff --git a/applications/SprPoP/src/pop_sfx.c b/applications/SprPoP/src/pop_sfx.c index 9bda4bd..d2a4449 100644 --- a/applications/SprPoP/src/pop_sfx.c +++ b/applications/SprPoP/src/pop_sfx.c @@ -263,6 +263,13 @@ void pop_sfx_play(uint8_t id) if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id; return; } + /* НАБОР ЕЩЁ ДОЧИТЫВАЕТСЯ. На старте программы звук открывается после + * ОДНОЙ страницы (в ней блок тишины), чтобы музыка заставки заиграла + * почти сразу; остальные приезжают позже (pop_sfx_init_begin/finish). + * Страницы под них уже выделены, но не прочитаны — эффект из такой + * страницы был бы шумом из чужих данных. Заявку на МУЗЫКУ выше это + * не трогает: её данные лежат отдельно. */ + if (!pop_snd_pages) return; if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1]) snd_next1 = (uint8_t)(id + 1); } diff --git a/applications/SprPoP/src/pop_sfx.h b/applications/SprPoP/src/pop_sfx.h index 42adc10..aba1e82 100644 --- a/applications/SprPoP/src/pop_sfx.h +++ b/applications/SprPoP/src/pop_sfx.h @@ -21,10 +21,28 @@ #include -/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ - * ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё - * дальнейшее молча ничего не делает, игра идёт без звука). */ -int pop_sfx_init(void) __banked; +/* ПОДЪЁМ НАБОРА РАЗБИТ НАДВОЕ — ради быстрого старта программы. + * + * Насосу, чтобы открыть CBL и заиграть МУЗЫКУ, нужен из всего набора + * ровно один блок — тишина, и упаковщик обязан класть её первым блоком + * СТРАНИЦЫ 0 (tools/pop_idx.py, проверка `_check_entry`). Поэтому: + * + * pop_sfx_init_begin() — индекс (snd.idx) + ОДНА страница (33 мс). + * После него можно звать pop_sfx_start() и + * играть музыку; эффектов ещё нет. + * pop_sfx_init_finish() — остальные страницы (~230 мс) и публикация + * pop_snd_pages: с этого момента есть эффекты. + * + * Пока набор неполон, pop_sfx_play() молча игнорирует эффекты (заявку на + * музыку принимает по-прежнему): страницы под них выделены, но ещё не + * прочитаны, и играть из них — это шум из чужих данных. + * + * ВЫВОД НИ ТА, НИ ДРУГАЯ НЕ ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — + * не получилось (тогда всё дальнейшее молча ничего не делает, игра идёт + * без звука; begin при отсутствии индекса поднимает набор из одной + * страницы тишины, чтобы не встала музыка). */ +int pop_sfx_init_begin(void) __banked; +int pop_sfx_init_finish(void) __banked; /* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт * чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить diff --git a/applications/SprPoP/src/pop_sfx_cold.c b/applications/SprPoP/src/pop_sfx_cold.c index 109cf48..2c20673 100644 --- a/applications/SprPoP/src/pop_sfx_cold.c +++ b/applications/SprPoP/src/pop_sfx_cold.c @@ -18,6 +18,11 @@ #include /* gfx_w0_map — залить блок тишины */ static uint8_t snd_blk; /* блок EMM под весь набор */ +/* Раздельная загрузка (см. шапку pop_sfx.h): сколько страниц обещал индекс + * и сколько уже прочитано. 0 в want — begin не проходил либо поднялась + * только страница тишины (набора нет), и дочитывать нечего. */ +static uint8_t snd_want_pages; +static uint8_t snd_done_pages; /* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- * * @@ -142,11 +147,13 @@ static int8_t snd_silence_only(void) return 0; } -int pop_sfx_init(void) __banked +/* ПЕРВАЯ ПОЛОВИНА: индекс + страница 0 (в ней блок тишины). После неё + * звук можно открывать, и музыка играет; эффекты — после finish. */ +int pop_sfx_init_begin(void) __banked { - uint8_t i, blk, pages; + uint8_t blk, pages; - if (pop_snd_ok) return 0; + if (snd_blk) return 0; /* набор уже поднимали */ /* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой * эффект лёг — знает только он. Набора без индекса не бывает: @@ -179,24 +186,58 @@ int pop_sfx_init(void) __banked if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) != (int8_t)pages) return -1; + /* Блок выделяем СРАЗУ на весь набор: номера страниц отдаёт + * mem_get_page по индексу, и finish дочитает в те же самые. */ blk = mem_alloc_pages(pages); if (!blk) { pop_arc_close(&arc); return -1; } - for (i = 0; i < pages; i++) { - pop_snd_page[i] = mem_get_page(blk, i); - if (!pop_snd_page[i] || - pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) { - pop_arc_close(&arc); - mem_free_block(blk); - return -1; - } + pop_snd_page[POP_SND_SILENCE_PAGE] = mem_get_page(blk, 0); + if (!pop_snd_page[POP_SND_SILENCE_PAGE] || + pop_arc_read(&arc, tbl, 0, 0, + pop_snd_page[POP_SND_SILENCE_PAGE], 0, 0) != 16384) { + pop_arc_close(&arc); + mem_free_block(blk); + return -1; } pop_arc_close(&arc); } snd_blk = blk; + snd_want_pages = pages; + snd_done_pages = 1; + /* pop_snd_pages НЕ ставим: набор ещё неполон, и до finish эффекты + * запускать нельзя (страницы выделены, но не прочитаны). */ + pop_snd_want = 1; /* набор есть — звук по умолчанию включён */ + return 0; +} + +/* ВТОРАЯ ПОЛОВИНА: дочитать остальные страницы и разрешить эффекты. + * Идемпотентна; архив открывается второй раз (51 мс) — это дешевле, чем + * держать открытый манипулятор через всю заставку при лимите в восемь. */ +int pop_sfx_init_finish(void) __banked +{ + pop_arc_t arc; + uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC]; + uint8_t i; + + if (pop_snd_pages) return 0; /* набор уже полный */ + if (!snd_want_pages) return -1; /* begin не проходил либо только тишина */ + if (snd_done_pages < snd_want_pages) { + if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) != + (int8_t)snd_want_pages) + return -1; + for (i = snd_done_pages; i < snd_want_pages; i++) { + pop_snd_page[i] = mem_get_page(snd_blk, i); + if (!pop_snd_page[i] || + pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) { + pop_arc_close(&arc); + return -1; /* страницы не отдаём: тишина в стр.0 жива */ + } + snd_done_pages = (uint8_t)(i + 1); + } + pop_arc_close(&arc); + } /* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан * видеть 0 и не пытаться ходить по полузаполненной таблице. */ - pop_snd_pages = pages; - pop_snd_want = 1; /* набор есть — звук по умолчанию включён */ + pop_snd_pages = snd_want_pages; return 0; } diff --git a/applications/SprPoP/src/pop_title.c b/applications/SprPoP/src/pop_title.c index 8b7f94c..7b1664c 100644 --- a/applications/SprPoP/src/pop_title.c +++ b/applications/SprPoP/src/pop_title.c @@ -21,6 +21,7 @@ #include "pop_pal.h" #include "pop_title.h" #include "pop_ui.h" +#include "sprpop_cold.h" /* pop_boot_sound/pop_boot_rest — фазы старта */ #define TITLE_W 320 #define TITLE_H 200 @@ -110,10 +111,14 @@ int pop_title_logo_ltr(void) __banked return pop_screen_present_ltr() ? 1 : 0; } +/* Ждать ticks кадров луча, доливая трек. Тема заставки играет с ПЕРВОЙ + * страницы (pop_music_stream), остальные обязаны приезжать по ходу — шаг + * стоит 16 мс и укладывается в кадровый интервал. */ static uint8_t title_wait(uint16_t ticks) { while (ticks--) { if (title_skip_pressed()) return 1; + if (pop_music_loading()) (void)pop_music_load_step(); gfx_wait_vsync(); } return 0; @@ -130,7 +135,8 @@ static void title_load_palette_dimmed(void) uint8_t pop_title_show(void) __banked { uint8_t step; - uint8_t skipped = 0, music_ok = 0; + uint8_t skipped = 0; + uint16_t first_wait = 0; /* такт 0 за вычетом времени загрузки */ int prepared; uint8_t cold_start = (uint8_t)(!title_shown && !pop_settings.show_build_info); @@ -154,22 +160,50 @@ uint8_t pop_title_show(void) __banked pop_screen_present((uint8_t)prepared); title_shown = 1; - /* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже — - * после fade_in, ровно как show_title: fade_in_2, затем - * play_sound(sound_54). */ - music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0); - + /* ЗДЕСЬ ЖЕ ДОГРУЖАЕТСЯ ВСЯ ИГРА — и это не случайное место. + * + * До 2026-09-02 pop_boot читал ВСЕ ресурсы перед первым пикселем: от + * запуска до проявления титула проходило несколько секунд чёрного + * экрана в тишине, потому что перед fade_in ещё и вычитывался целиком + * трек заставки (248 КБ). Теперь порядок обратный и совпадает с + * оригиналом (init_game_main грузит до заставки только мелочь, + * seg000.c:156): + * 1) картинка титула и fade_in — первое, что происходит; + * 2) звук: индекс + ОДНА страница набора, CBL (pop_boot_sound); + * 3) тема — с первой же страницы (pop_music_stream), не дожидаясь + * всех шестнадцати; + * 4) остальные ресурсы — под стоящим титулом и звучащей темой. + * + * Порядок «fade_in, потом тема» сохранён как у show_title (fade_in_2, + * затем play_sound(sound_54)). */ for (step = 0; step < sizeof(title_scene); step++) { if (step && title_draw(title_scene[step], 1) < 0) break; if (step == 0) { + uint8_t t0; + if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; } - if (music_ok) { - (void)pop_sfx_start(); - pop_music_play(); + (void)pop_boot_sound(); + (void)pop_music_stream(POP_MUS_INTRO); + /* ЧЕМ ЗАПЛАЧЕН ПЕРВЫЙ ТАКТ. Загрузка занимает около секунды, и + * без поправки титул простоял бы на первом кадре дольше + * оригинала, а сцена уехала бы относительно темы. Считаем + * потраченное часами насоса (85,4 Гц, идут от расхода буфера + * CBL — единственные, кому не мешает диск) и переводим в кадры + * луча сдвигами: 48,83/85,4 = 0,572 ~ 1/2 + 1/16. Без звука + * часы стоят, поправка выходит нулевой — и правильно, тогда и + * синхронизировать не с чем. */ + t0 = pop_snd_tick; + (void)pop_boot_rest(); + { + uint16_t spent = (uint8_t)(pop_snd_tick - t0); + + spent = (uint16_t)((spent >> 1) + (spent >> 4)); + first_wait = (spent >= title_wait_ticks[0]) + ? 0 : (uint16_t)(title_wait_ticks[0] - spent); } } - if (title_wait(title_wait_ticks[step])) { + if (title_wait(step ? title_wait_ticks[step] : first_wait)) { skipped = 1; break; } diff --git a/applications/SprPoP/src/sprpop_cold.c b/applications/SprPoP/src/sprpop_cold.c index 5971e3f..3ece899 100644 --- a/applications/SprPoP/src/sprpop_cold.c +++ b/applications/SprPoP/src/sprpop_cold.c @@ -1114,6 +1114,36 @@ void pop_flip_screen(void) __banked * уровень). В резиденте от них оставались только вызовы — сам код здесь. */ +/* Фазы старта (см. шапку pop_boot): обе половины идемпотентны, а + * boot_rest_fail нужен, чтобы повторный вызов не пытался читать заново + * то, что уже не прочиталось. */ +static uint8_t boot_sound_done; +static uint8_t boot_rest_done; +static uint8_t boot_rest_fail; + +/* СТАРТ РАЗБИТ НА ТРИ ФАЗЫ — чтобы игрок увидел заставку и услышал музыку + * сразу, а не через несколько секунд чёрного экрана в тишине. + * + * pop_boot() — только настройки и графика: чёрный экран за ~0,1 с. + * pop_boot_sound() — индекс+первая страница звука, индекс музыки, CBL: + * после него титул уже показан, и тема заставки может + * начать играть, не дожидаясь остальных ресурсов. + * pop_boot_rest() — всё тяжёлое (эффекты, Кид, Тень, шрифт) ПОД стоящим + * титулом и звучащей темой; идемпотентно. + * + * ЧЕТЫРЕ ЗАГРУЗКИ ОТСЮДА ПРОСТО УБРАНЫ, а не отложены: атлас фона, страж, + * уровень 1 и игровая палитра — это дубликаты того, что и так делает + * pop_level_switch() на входе в ЛЮБОЙ уровень (а pop_new_game_load ещё и + * pop_pal_game_load перед ним). Оба маршрута в игру, LEVEL_LOAD и DEMO, + * упираются в pop_level_switch, так что второй раз читать их до заставки + * незачем — это ~0,77 с на ровном месте. Так же устроен и оригинал: у + * SDLPoP chtab_5/6/7 грузит load_lev_spr() на старте уровня, а init_game_main + * до заставки читает только меч, пламя, звуки и палитры (seg000.c:156). + * + * ПАЛИТРУ ТРОГАТЬ НЕЛЬЗЯ, пока на экране титул: и kid.pal (все 256 записей), + * и палитра тайлсета пишутся прямо в железо и перекрасили бы заставку. + * Поэтому в отложенной части нет ни одной палитровой операции — они все + * живут на маршруте входа в уровень, за чёрным экраном. */ int pop_boot(void) __banked { /* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */ @@ -1141,77 +1171,105 @@ int pop_boot(void) __banked gfx_set_visible_page(0); pop_pal_black(); - /* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после - * initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к - * этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */ + /* FONT.ATL — В МИНИМАЛЬНОЙ ФАЗЕ, И ОТКЛАДЫВАТЬ ЕГО НЕЛЬЗЯ. + * + * Дело не в шрифте: СНИМОК ПАЛИТРЫ физически живёт в хвосте страницы + * шрифта (pop_ui.c, POP_FONT_SNAPSHOT_OFF — в W2 килобайта нет), и вся + * машинерия яркости начинается с `if (!font_ready) return`. Без него + * pop_pal_apply(DIM_BLACK) не гасит, а fade_in нечего проявлять — + * заставка возникает разом на полной яркости (поймано пользователем + * сразу после первой версии этой перестановки, 2026-09-02). + * Цена вопроса — 12 КБ, около 75 мс. */ + if (pop_menu_init() != 0) { + puts("menu font atlas load failed"); + return -1; + } + pop_current_level = FIRST_LEVEL; - if (pop_bg_load(pop_level_type()) != 0) { - puts("pop_bg_load failed"); - return -1; - } - /* pop_bg_load установил только свои слоты палитры. До конца загрузки - * всё равно держим экран чёрным. */ - pop_pal_black(); pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */ - /* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */ pop_guard_reset(); - if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */ - puts("kid.ani load failed\n"); - return -1; - } - pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ - pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ - pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */ + pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ + pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ + pop_checkpoint = 0; /* init_game (seg003:37) */ + if (!pop_settings.show_build_info) pop_pal_apply(POP_PAL_DIM_BLACK); + return 0; +} + +/* ЗВУК ВПЕРЁД ОСТАЛЬНЫХ РЕСУРСОВ. Насосу для тишины (а значит и для того, + * чтобы CBL вообще открылся) нужен ровно ОДИН блок набора, и упаковщик + * обязан класть его первым блоком страницы 0 — поэтому здесь читается одна + * страница вместо восьми (33 мс против 440). Остальные приезжают в + * pop_boot_rest, а до тех пор pop_sfx_play молча пропускает эффекты. + * + * Звать ПОСЛЕ того, как титул уже проявлен: чтение всё равно занимает + * полсекунды, и лучше, чтобы она прошла под картинкой. */ +int pop_boot_sound(void) __banked +{ + if (boot_sound_done) return 0; + boot_sound_done = 1; + (void)pop_sfx_init_begin(); /* эффектов может и не быть — не фатально */ /* Индекс музыкального набора — одна страница EMM. Без него музыки не * будет (pop_music_info вернёт -1 на любой трек), но игра работает: * это необязательный ресурс, как и сами треки. */ (void)pop_music_index_load(); pop_settings_apply(1); /* CFG -> speed/cheats/CBL */ + (void)pop_sfx_start(); + return 0; +} + +/* Шаг подкачки трека между кусками загрузки: заставочная тема играет уже + * с первой страницы (pop_music_stream), остальные обязаны приезжать по + * ходу — иначе насос уйдёт в ещё не прочитанную страницу. Запас после + * шага — 0,75 с звучания на каждые 8 КБ, куски загрузки короче. */ +static void boot_music_step(void) +{ + if (pop_music_loading()) (void)pop_music_load_step(); +} + +int pop_boot_rest(void) __banked +{ + if (boot_rest_done) return boot_rest_fail ? -1 : 0; + boot_rest_done = 1; + /* Заставку могли пропустить в первую же секунду — тогда звук ещё не + * поднимался. Идемпотентно. */ + (void)pop_boot_sound(); + + (void)pop_sfx_init_finish(); /* дочитать эффекты (музыка уже играет) */ + boot_music_step(); + if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */ + puts("kid.ani load failed\n"); + boot_rest_fail = 1; + return -1; + } + boot_music_step(); + pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ + boot_music_step(); if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ puts("kid atlas load failed"); + boot_rest_fail = 1; return -1; } - if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ - puts("level load failed"); - return -1; - } - pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ - pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ - pop_checkpoint = 0; /* init_game (seg003:37) */ + boot_music_step(); - /* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной - * палитре и поэтому не открывает игроку старое содержимое VRAM. */ - if (pop_menu_init() != 0) { - puts("menu font atlas load failed"); - return -1; - } - /* kid.pal + динамические slots tileset/Shadow — один логический - * источник. pop_pal_game_load делает снимок ПОСЛЕ всех доплат. */ - pop_pal_game_load(); - /* Сохраняем ИМЕННО игровую палитру: после стартового title/intro она - * понадобится как источник обычного fade-in. Для build-info экран - * остаётся штатный яркий игровой фон. */ - if (!pop_settings.show_build_info) { - pop_pal_apply(POP_PAL_DIM_BLACK); - } - - /* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново - * выполняет pop_level_switch. Если нарисовать её сейчас в обе страницы, - * она становится сохранённой VRAM и может мелькнуть перед первым title - * при следующем запуске приложения. Единственное исключение — явно - * включённый build-info: его фон по контракту является игровой сценой. */ + /* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно выполняет + * pop_level_switch. Единственное исключение — явно включённый + * build-info: его фон по контракту является игровой сценой, поэтому + * только здесь читаются фон с уровнем и применяется игровая палитра. */ if (pop_settings.show_build_info) { + if (pop_bg_load(pop_level_type()) != 0 || + pop_level_load_num(FIRST_LEVEL) != 0) { + puts("build-info scene load failed"); + boot_rest_fail = 1; + return -1; + } + pop_guard_load(); + pop_pal_game_load(); pop_start_level(); if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); gfx_set_draw_page(0); pop_char_draw(POP_CH_KID); pop_char_fore(POP_CH_KID); } - /* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы, - * уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск - * надолго, насос не успевает долить блок, и на слух это скрежет - * (pop_sfx.h). Парная к pop_sfx_pause в pop_level_switch. */ - pop_sfx_start(); return 0; } @@ -1224,6 +1282,11 @@ int pop_level_switch(void) __banked uint8_t old_page; gfx_rect_t full; + /* РЕСУРСЫ МОГУТ БЫТЬ ЕЩЁ НЕ ПРОЧИТАНЫ. Кид, Тень и шрифт грузятся под + * заставкой (см. шапку pop_boot), а её можно пропустить клавишей в + * первую же секунду. Идемпотентно: со второго раза это проверка байта. */ + if (pop_boot_rest() != 0) return -1; + /* Граница FG0: повреждённый snapshot или старый внешний вызов не должен * даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING * добавит автомат приложения (FG11); пока просто оставляем уровень. */ @@ -1368,6 +1431,10 @@ int pop_new_game_load(void) __banked gfx_wait_vsync(); } + /* ДО pop_pal_game_load: она доплачивает слоты палитры по загруженным + * наборам (Тень, страж, тайлсет), и звать её раньше их чтения нельзя. */ + if (pop_boot_rest() != 0) return -1; + pop_kid_hp_newgame(); hitp_max = POP_START_HITP; pop_timer_new_game(); diff --git a/applications/SprPoP/src/sprpop_cold.h b/applications/SprPoP/src/sprpop_cold.h index a0c0c62..b51384b 100644 --- a/applications/SprPoP/src/sprpop_cold.h +++ b/applications/SprPoP/src/sprpop_cold.h @@ -83,9 +83,21 @@ void pop_flip_screen(void) __banked; * Три куска главного цикла с частотой «раз за игру» или «раз на уровень». * В резиденте от них остались только вызовы. */ -/* Загрузить ресурсы, поднять графику и палитру, поставить Кида на старт и - * нарисовать первый экран. 0 — OK, -1 — фатально (сообщение уже выведено). */ +/* СТАРТ В ТРИ ФАЗЫ — ради быстрого появления заставки; подробности и + * причины (в том числе почему часть загрузок убрана совсем) — в шапке + * pop_boot в sprpop_cold.c. 0 — OK, -1 — фатально (сообщение выведено). + * + * pop_boot() настройки + графика, чёрный экран; + * pop_boot_sound() индекс+первая страница звука, индекс музыки, CBL — + * звать, когда титул уже на экране: после него можно + * пускать тему заставки; + * pop_boot_rest() всё тяжёлое (эффекты, Кид, Тень, шрифт) под стоящим + * титулом; идемпотентно, страхуется вызовом из + * pop_new_game_load/pop_level_switch на случай, если + * заставку пропустили сразу. */ int pop_boot(void) __banked; +int pop_boot_sound(void) __banked; +int pop_boot_rest(void) __banked; /* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет, * старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан