Compare commits

...

2 Commits

Author SHA1 Message Date
snark13 7377bf6a2c SprPoP: заставка и музыка сразу после запуска, а не через несколько секунд
От запуска до проявления титула проходило ~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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:30:53 +03:00
snark13 b34997073e libc/cbl: выключение CBL больше не оставляет железо петь одну ноту
cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит
ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу),
bit7 остаётся включённым.  А буфер CBL (256 слотов) при включённом bit7
крутится по кругу и не чистится ничем — железо бесконечно поёт хвост
последнего сэмпла, тоном и громкостью по последней мелодии.  Пойман
пользователем на железе: Ctrl+S во время музыки и пропуск заставки
давали ноту до следующего cbl_open.

В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у
которого divs[2]==0 и таймер не заводится вовсе.  На железе reserved-коды
не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора,
а не железа.

Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА:
- _cbl_port_sync() после каждой записи в порт управления зовёт
  _cbl_prime(0x80).  Это закрывает и паузу звука, и включение bit7 ради
  луча на холодном старте (в буфере лежал мусор от прошлой программы), и
  полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП
  обычного COVOX и центруют его, снимая щелчок;
- _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8
  (7,8125 кГц): поведение определено и на железе, и в MAME, прерывания
  по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит
  только при живом хуке насоса;
- cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе
  насос долил бы буфер уже после заливки.

Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime
следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие —
эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены.  Рост atlas в
size-check к этой правке отношения не имеет (замерен тот же и без неё).

Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME
(титры → пропуск заставки → уровень 1, пейсинг по лучу жив).  Сам баг
проверяется только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 10:55:15 +03:00
11 changed files with 340 additions and 99 deletions
+7
View File
@@ -263,6 +263,13 @@ void pop_sfx_play(uint8_t id)
if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id; if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id;
return; return;
} }
/* НАБОР ЕЩЁ ДОЧИТЫВАЕТСЯ. На старте программы звук открывается после
* ОДНОЙ страницы (в ней блок тишины), чтобы музыка заставки заиграла
* почти сразу; остальные приезжают позже (pop_sfx_init_begin/finish).
* Страницы под них уже выделены, но не прочитаны — эффект из такой
* страницы был бы шумом из чужих данных. Заявку на МУЗЫКУ выше это
* не трогает: её данные лежат отдельно. */
if (!pop_snd_pages) return;
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1]) if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
snd_next1 = (uint8_t)(id + 1); snd_next1 = (uint8_t)(id + 1);
} }
+22 -4
View File
@@ -21,10 +21,28 @@
#include <stdint.h> #include <stdint.h>
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ /* ПОДЪЁМ НАБОРА РАЗБИТ НАДВОЕ — ради быстрого старта программы.
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё *
* дальнейшее молча ничего не делает, игра идёт без звука). */ * Насосу, чтобы открыть CBL и заиграть МУЗЫКУ, нужен из всего набора
int pop_sfx_init(void) __banked; * ровно один блок — тишина, и упаковщик обязан класть её первым блоком
* СТРАНИЦЫ 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). Звать ПОСЛЕ всей загрузки: пока идёт /* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить * чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
+54 -13
View File
@@ -18,6 +18,11 @@
#include <sprite.h> /* gfx_w0_map — залить блок тишины */ #include <sprite.h> /* gfx_w0_map — залить блок тишины */
static uint8_t snd_blk; /* блок EMM под весь набор */ 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) ----------------------- * /* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- *
* *
@@ -142,11 +147,13 @@ static int8_t snd_silence_only(void)
return 0; 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) != if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) !=
(int8_t)pages) (int8_t)pages)
return -1; return -1;
/* Блок выделяем СРАЗУ на весь набор: номера страниц отдаёт
* mem_get_page по индексу, и finish дочитает в те же самые. */
blk = mem_alloc_pages(pages); blk = mem_alloc_pages(pages);
if (!blk) { pop_arc_close(&arc); return -1; } if (!blk) { pop_arc_close(&arc); return -1; }
for (i = 0; i < pages; i++) { pop_snd_page[POP_SND_SILENCE_PAGE] = mem_get_page(blk, 0);
pop_snd_page[i] = mem_get_page(blk, i); if (!pop_snd_page[POP_SND_SILENCE_PAGE] ||
if (!pop_snd_page[i] || pop_arc_read(&arc, tbl, 0, 0,
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) { pop_snd_page[POP_SND_SILENCE_PAGE], 0, 0) != 16384) {
pop_arc_close(&arc); pop_arc_close(&arc);
mem_free_block(blk); mem_free_block(blk);
return -1; return -1;
}
} }
pop_arc_close(&arc); pop_arc_close(&arc);
} }
snd_blk = blk; 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 и не пытаться ходить по полузаполненной таблице. */ * видеть 0 и не пытаться ходить по полузаполненной таблице. */
pop_snd_pages = pages; pop_snd_pages = snd_want_pages;
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
return 0; return 0;
} }
+44 -10
View File
@@ -21,6 +21,7 @@
#include "pop_pal.h" #include "pop_pal.h"
#include "pop_title.h" #include "pop_title.h"
#include "pop_ui.h" #include "pop_ui.h"
#include "sprpop_cold.h" /* pop_boot_sound/pop_boot_rest — фазы старта */
#define TITLE_W 320 #define TITLE_W 320
#define TITLE_H 200 #define TITLE_H 200
@@ -110,10 +111,14 @@ int pop_title_logo_ltr(void) __banked
return pop_screen_present_ltr() ? 1 : 0; return pop_screen_present_ltr() ? 1 : 0;
} }
/* Ждать ticks кадров луча, доливая трек. Тема заставки играет с ПЕРВОЙ
* страницы (pop_music_stream), остальные обязаны приезжать по ходу — шаг
* стоит 16 мс и укладывается в кадровый интервал. */
static uint8_t title_wait(uint16_t ticks) static uint8_t title_wait(uint16_t ticks)
{ {
while (ticks--) { while (ticks--) {
if (title_skip_pressed()) return 1; if (title_skip_pressed()) return 1;
if (pop_music_loading()) (void)pop_music_load_step();
gfx_wait_vsync(); gfx_wait_vsync();
} }
return 0; return 0;
@@ -130,7 +135,8 @@ static void title_load_palette_dimmed(void)
uint8_t pop_title_show(void) __banked uint8_t pop_title_show(void) __banked
{ {
uint8_t step; uint8_t step;
uint8_t skipped = 0, music_ok = 0; uint8_t skipped = 0;
uint16_t first_wait = 0; /* такт 0 за вычетом времени загрузки */
int prepared; int prepared;
uint8_t cold_start = (uint8_t)(!title_shown && uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info); !pop_settings.show_build_info);
@@ -154,22 +160,50 @@ uint8_t pop_title_show(void) __banked
pop_screen_present((uint8_t)prepared); pop_screen_present((uint8_t)prepared);
title_shown = 1; title_shown = 1;
/* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже — /* ЗДЕСЬ ЖЕ ДОГРУЖАЕТСЯ ВСЯ ИГРА — и это не случайное место.
* после fade_in, ровно как show_title: fade_in_2, затем *
* play_sound(sound_54). */ * До 2026-09-02 pop_boot читал ВСЕ ресурсы перед первым пикселем: от
music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0); * запуска до проявления титула проходило несколько секунд чёрного
* экрана в тишине, потому что перед 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++) { for (step = 0; step < sizeof(title_scene); step++) {
if (step && title_draw(title_scene[step], 1) < 0) if (step && title_draw(title_scene[step], 1) < 0)
break; break;
if (step == 0) { if (step == 0) {
uint8_t t0;
if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; } if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; }
if (music_ok) { (void)pop_boot_sound();
(void)pop_sfx_start(); (void)pop_music_stream(POP_MUS_INTRO);
pop_music_play(); /* ЧЕМ ЗАПЛАЧЕН ПЕРВЫЙ ТАКТ. Загрузка занимает около секунды, и
* без поправки титул простоял бы на первом кадре дольше
* оригинала, а сцена уехала бы относительно темы. Считаем
* потраченное часами насоса (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; skipped = 1;
break; break;
} }
+118 -51
View File
@@ -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 int pop_boot(void) __banked
{ {
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */ /* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
@@ -1141,77 +1171,105 @@ int pop_boot(void) __banked
gfx_set_visible_page(0); gfx_set_visible_page(0);
pop_pal_black(); pop_pal_black();
/* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после /* FONT.ATL — В МИНИМАЛЬНОЙ ФАЗЕ, И ОТКЛАДЫВАТЬ ЕГО НЕЛЬЗЯ.
* initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к *
* этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */ * Дело не в шрифте: СНИМОК ПАЛИТРЫ физически живёт в хвосте страницы
* шрифта (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; 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_cd_init(); /* метка «фон трогали» — пустые диапазоны */
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
pop_guard_reset(); pop_guard_reset();
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */ pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
puts("kid.ani load failed\n"); pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
return -1; pop_checkpoint = 0; /* init_game (seg003:37) */
} if (!pop_settings.show_build_info) pop_pal_apply(POP_PAL_DIM_BLACK);
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ return 0;
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ }
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
/* ЗВУК ВПЕРЁД ОСТАЛЬНЫХ РЕСУРСОВ. Насосу для тишины (а значит и для того,
* чтобы 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. Без него музыки не /* Индекс музыкального набора — одна страница EMM. Без него музыки не
* будет (pop_music_info вернёт -1 на любой трек), но игра работает: * будет (pop_music_info вернёт -1 на любой трек), но игра работает:
* это необязательный ресурс, как и сами треки. */ * это необязательный ресурс, как и сами треки. */
(void)pop_music_index_load(); (void)pop_music_index_load();
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */ 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) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed"); puts("kid atlas load failed");
boot_rest_fail = 1;
return -1; return -1;
} }
if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ boot_music_step();
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) */
/* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной /* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно выполняет
* палитре и поэтому не открывает игроку старое содержимое VRAM. */ * pop_level_switch. Единственное исключение — явно включённый
if (pop_menu_init() != 0) { * build-info: его фон по контракту является игровой сценой, поэтому
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: его фон по контракту является игровой сценой. */
if (pop_settings.show_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(); pop_start_level();
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0); gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID); pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID); pop_char_fore(POP_CH_KID);
} }
/* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы,
* уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск
* надолго, насос не успевает долить блок, и на слух это скрежет
* (pop_sfx.h). Парная к pop_sfx_pause в pop_level_switch. */
pop_sfx_start();
return 0; return 0;
} }
@@ -1224,6 +1282,11 @@ int pop_level_switch(void) __banked
uint8_t old_page; uint8_t old_page;
gfx_rect_t full; gfx_rect_t full;
/* РЕСУРСЫ МОГУТ БЫТЬ ЕЩЁ НЕ ПРОЧИТАНЫ. Кид, Тень и шрифт грузятся под
* заставкой (см. шапку pop_boot), а её можно пропустить клавишей в
* первую же секунду. Идемпотентно: со второго раза это проверка байта. */
if (pop_boot_rest() != 0) return -1;
/* Граница FG0: повреждённый snapshot или старый внешний вызов не должен /* Граница FG0: повреждённый snapshot или старый внешний вызов не должен
* даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING * даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING
* добавит автомат приложения (FG11); пока просто оставляем уровень. */ * добавит автомат приложения (FG11); пока просто оставляем уровень. */
@@ -1368,6 +1431,10 @@ int pop_new_game_load(void) __banked
gfx_wait_vsync(); gfx_wait_vsync();
} }
/* ДО pop_pal_game_load: она доплачивает слоты палитры по загруженным
* наборам (Тень, страж, тайлсет), и звать её раньше их чтения нельзя. */
if (pop_boot_rest() != 0) return -1;
pop_kid_hp_newgame(); pop_kid_hp_newgame();
hitp_max = POP_START_HITP; hitp_max = POP_START_HITP;
pop_timer_new_game(); pop_timer_new_game();
+14 -2
View File
@@ -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(void) __banked;
int pop_boot_sound(void) __banked;
int pop_boot_rest(void) __banked;
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет, /* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан * старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
+4 -4
View File
@@ -15,9 +15,9 @@ bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 5860 cblstream 5862
cbltest 6006 cbltest 6008
cblwav 6099 cblwav 6101
conio 4605 conio 4605
conio2 3929 conio2 3929
convbench 3508 convbench 3508
@@ -28,7 +28,7 @@ fdmax 6023
filetest 10547 filetest 10547
fpsdiv 4870 fpsdiv 4870
gets 509 gets 509
gfx_dbuf 5131 gfx_dbuf 5142
gfx_demo 4114 gfx_demo 4114
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
15 blitperf
16 blitw
17 cat
18 cblstream
19 cbltest
20 cblwav
21 conio
22 conio2
23 convbench
28 filetest
29 fpsdiv
30 gets
31 gfx_dbuf
32 gfx_demo
33 gfxbanks
34 hello
+31 -9
View File
@@ -26,12 +26,29 @@
* *
* _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих * _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих
* держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL- * держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL-
* сессии. Нужно gfx_wait_vsync() (см. libc/gfx/gfx_wait_vsync.c): бит * сессии. Нужно gfx_wait_vsync() (см. libbgi/common/gfx_wait_vsync.c):
* 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp kbd_fe_r), * бит 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp
* но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный CBL уже * kbd_fe_r), но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный
* открыт — портом владеет он, ref/unref его не трогают; иначе включают * CBL уже открыт — портом владеет он, ref/unref его не трогают; иначе
* bit7 "немым" кодом частоты _CBL_VSYNC_FREQ (таймер CBL не заводится * включают bit7 "немым" кодом частоты _CBL_VSYNC_FREQ и заливают
* — divs[2]=0 в MAME — ни звука, ни прерываний, только синхро-бит). * аппаратный буфер тишиной — БЕЗ ЗАЛИВКИ ЖЕЛЕЗО ПОЁТ (см. ниже).
*
* ЖЕЛЕЗО ПОЁТ ТО, ЧТО ЛЕЖИТ В БУФЕРЕ. Буфер CBL — 256 слотов, которые
* при включённом bit7 крутятся ПО КРУГУ и которые никто не чистит;
* фирменный пример Sprinter Team (docs/converted/Forum.txt, "Самый
* простой пример") на этом и построен: залил 256 отсчётов — "должен
* пищать одну ноту". Значит любое состояние "bit7 включён, а насос не
* работает" — это бесконечная нота из хвоста последнего сэмпла.
*
* В MAME этого не видно: там код частоты 2 не заводил таймер вовсе
* (divs[2]=0, sprinter.cpp:822), поэтому "немой" режим был тих даром.
* На железе код 2 — reserved (Forum.txt:704), поведение не определено, и
* ЦАП тактируется: пользователь поймал бесконечную ноту после Ctrl+S и
* после пропуска заставки (2026-09-02). Поэтому теперь: (1) частота —
* документированная, самая медленная; (2) буфер ВСЕГДА оставляем залитым
* тишиной — и на входе в "немой" режим, и при полном выключении (там те
* же записи центруют ЦАП обычного COVOX, bit7=0 → порт данных идёт прямо
* в ЦАП). Цена — 256 OUT'ов (~0,3 мс) на редкое событие.
*/ */
#ifndef _CBL_INTERNAL_H #ifndef _CBL_INTERNAL_H
#define _CBL_INTERNAL_H #define _CBL_INTERNAL_H
@@ -42,7 +59,10 @@
#define _CBL_CTRL_PORT 0x004E #define _CBL_CTRL_PORT 0x004E
#define _CBL_DATA_PORT 0x4F #define _CBL_DATA_PORT 0x4F
#define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */ #define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */
#define _CBL_VSYNC_FREQ 0x02 /* "немой" код частоты — divs[2]=0 в MAME, таймер не идёт */ #define _CBL_VSYNC_FREQ 0x08 /* "немой" режим: самый медленный ДОКУМЕНТИРОВАННЫЙ
* код (7,8125 кГц). Тишина обеспечена не
* остановкой таймера (это свойство MAME, не
* железа), а залитым тишиной буфером. */
extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */ extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */
extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */ extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */
@@ -62,8 +82,10 @@ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill); uint8_t underrun_mode, cbl_fill_fn fill);
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после /* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше. * ЛЮБОЙ записи в порт управления: при включении — иначе первые 23 мс
* Подробный разбор — в шапке _cbl_prime.c. */ * играет то, что лежало в буфере раньше; при выключении — иначе железо
* крутит хвост последнего сэмпла бесконечной нотой. Подробный разбор —
* в шапке _cbl_prime.c. */
void _cbl_prime(uint8_t silence); void _cbl_prime(uint8_t silence);
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один /* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
+20 -3
View File
@@ -7,9 +7,23 @@
* *
* Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет * Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет
* он, ref/unref его не трогают вообще (это уже нужное состояние). * он, ref/unref его не трогают вообще (это уже нужное состояние).
* Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ * Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ.
* — таймер CBL при этом коде не заводится (divs[2]=0 в MAME), значит *
* ни звука, ни прерываний, только доступ к синхро-биту. * ТИШИНУ ДАЁТ ЗАЛИТЫЙ БУФЕР, А НЕ ОСТАНОВЛЕННЫЙ ТАЙМЕР. Раньше здесь
* стоял reserved-код частоты 2 — ровно потому, что в MAME при нём не
* заводится таймер (divs[2]=0). На железе поведение reserved-кодов не
* определено, ЦАП тактируется, и включённый bit7 поверх старого буфера
* даёт бесконечную ноту из хвоста последнего сэмпла (пойман Ctrl+S во
* время музыки, 2026-09-02; разбор — в шапке _cbl.h). Поэтому после
* КАЖДОЙ записи в порт управления заливаем все 256 слотов тишиной:
* - bit7 включён — крутится тишина (беззвучно на любом железе);
* - bit7 выключен — те же записи уходят в ЦАП обычного COVOX и
* центруют его на 0x80, снимая щелчок/постоянное смещение.
* Оба состояния редкие (старт пейсинга, пауза звука, выход), 256 OUT'ов
* — около 0,3 мс.
*
* Цена для ГРАФИЧЕСКОЙ программы без звука: _cbl_prime (~10 Б кода)
* приезжает следом за gfx_wait_vsync. Дешевле, чем гудящее железо.
*/ */
#include "_cbl.h" #include "_cbl.h"
@@ -19,6 +33,9 @@ void _cbl_port_sync(void)
{ {
if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */ if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */
_cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0); _cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0);
/* Формат "немого" режима всегда 8-битный (bit5 не выставлен), поэтому
* тишина здесь — 0x80 независимо от того, каким был закрытый сеанс. */
_cbl_prime(0x80);
} }
void _cbl_port_ref(void) void _cbl_port_ref(void)
+15 -1
View File
@@ -1,5 +1,6 @@
/* /*
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после включения. * _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после записи в
* порт управления: и при включении, и при выключении.
* *
* ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни * ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни
* при сбросе, ни при записи в порт управления — там остаётся то, что лежало * при сбросе, ни при записи в порт управления — там остаётся то, что лежало
@@ -31,6 +32,19 @@
* Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый * Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый
* сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для * сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для
* 8-бит беззнакового, 0x00 для 16-бит. * 8-бит беззнакового, 0x00 для 16-бит.
*
* ВТОРАЯ РОЛЬ — ГАШЕНИЕ (добавлена 2026-09-02, _cbl_port_sync). Запись
* в порт управления НЕ чистит буфер и в обратную сторону: пока bit7
* включён (а его держит gfx_wait_vsync ради бита луча), железо крутит
* оставшиеся 256 байт по кругу — это ровно "одна нота" из фирменного
* примера Forum.txt, только незваная. Поэтому после каждой записи в
* порт управления зовём эту же функцию:
* - bit7 остался включён — по кругу крутится тишина;
* - bit7 снят — те же 256 записей идут прямо в ЦАП обычного COVOX
* (MAME sprinter.cpp, `case 0x88` без cbl_mode()) и оставляют его в
* нуле 0x80, а не на последнем сэмпле.
* Гонки с насосом нет и здесь: при гашении хук уже снят, при включении
* первое прерывание будет только на 128-м слоте.
*/ */
#include "_cbl.h" #include "_cbl.h"
+11 -2
View File
@@ -10,7 +10,16 @@
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync() * Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит * пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
* ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита * ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита
* синхронизации; иначе порт гасится полностью. * синхронизации; иначе порт гасится полностью. Он же заливает
* аппаратный буфер тишиной — БЕЗ ЭТОГО ЗАКРЫТИЕ CBL НЕ ЗАКРЫВАЕТ ЗВУК:
* пока bit7 держит vsync-ссылка, железо крутит по кругу оставшиеся в
* буфере 256 байт последнего сэмпла и поёт ноту до следующего
* cbl_open() (пойман Ctrl+S во время музыки, 2026-09-02; полный разбор
* — в шапке _cbl.h). В MAME это не воспроизводится.
*
* Порядок обязателен: сначала снять хук (иначе насос долил бы буфер уже
* после заливки), и только потом _cbl_port_sync с заливкой — вся
* скобка под DI, чтобы между снятием хука и тишиной ничего не влезло.
*/ */
#include "_cbl.h" #include "_cbl.h"
@@ -21,7 +30,7 @@ void cbl_close(void)
if (!_irq_cbl_hook) return; if (!_irq_cbl_hook) return;
IRQ_DISABLE(); IRQ_DISABLE();
_irq_cbl_hook = 0; _irq_cbl_hook = 0;
_cbl_port_sync(); /* режим порта + заливка буфера тишиной */
IRQ_ENABLE(); IRQ_ENABLE();
_cbl_port_sync();
_irq_table_unref(); _irq_table_unref();
} }