Compare commits

..

4 Commits

Author SHA1 Message Date
snark13 31d0075090 SprPoP: набор звуковых эффектов пересобран из MSDOS-оцифровки
make SND_SRC=msdos MUSIC_FMT=flac resources-rebuild — полная перегенерация
всех ресурсов.  Изменился только SND/: у MSDOS-набора оцифровка полнее,
чем у SDLPoP (там пуст звук 48 spiked и короче 10 sword_vs_sword — разбор
в docs/sound_plan.md).

Музыка и остальные архивы после перегенерации совпали с закоммиченными
байт в байт — набор flac и есть тот, из которого они сделаны.

Умолчание сборки НЕ меняется: SND_SRC ?= sdlpop, чтобы клон без
оригинального дистрибутива DOS собирался целиком (make fetch).  Вернуть
прежний набор — make resources-sound после make clean stamps, или
make SND_SRC=sdlpop -B resources-sound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:45:17 +03:00
snark13 6176f6da31 SprPoP: Restart Game возвращал заставку без музыки и вешал её на титуле
pop_boot_sound() был целиком под разовым флагом, а pop_sfx_start() стоял
внутри него.  До перестановки старта CBL открывал сам шаг 0 титров, и
делал это КАЖДЫЙ раз; теперь второй вызов уходил по раннему return, а
pop_title_show начинается с pop_sfx_pause() — значит после Restart Game
насос оставался выключенным и заставка шла молча.

Тем же корнем объясняется зависание: с закрытым CBL курсор трека не
двигается, pop_mus_left не убывает, и ожидания «дослушать тему» в титрах
и интро (while (pop_music_busy())) висели до нажатия клавиши — на экране
Prince of Persia.

- pop_boot_sound: под разовым флагом осталась только ЗАГРУЗКА (индекс
  звука, первая страница набора, индекс музыки, settings_apply);
  pop_sfx_start() зовётся всегда — он идемпотентен и уважает Ctrl+S;
- pop_music_busy(): признак теперь «звучит», а не «есть курсор» —
  спрашивает и про открытый вывод (pop_snd_ok).  Ждать неиграющую музыку
  нельзя в принципе, и эта строка закрывает весь класс подвисаний.

Проверено в MAME: уровень 1 -> ESC -> RESTART GAME -> титул с музыкой
(pop_snd_ok=1, курсор трека прошёл страницы 8..16, pop_mus_id=54), дальше
последовательность сама уходит в сцену с принцессой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 11:45:02 +03:00
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
14 changed files with 367 additions and 108 deletions
Binary file not shown.
Binary file not shown.
+10 -1
View File
@@ -17,6 +17,7 @@
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
#include "pop_music.h"
#include "pop_sfx.h"
#include "_pop_sfx.h" /* pop_snd_ok — открыт ли вывод (см. pop_music_busy) */
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
#include "_pop_file.h"
@@ -454,7 +455,15 @@ void pop_music_service(void) __banked
uint8_t pop_music_busy(void) __banked
{
return (uint8_t)(pop_mus_left != 0);
/* «ЗВУЧИТ», А НЕ «ЕСТЬ КУРСОР». Курсор трека двигает насос CBL, и пока
* вывод закрыт (pop_sfx_pause на загрузке уровня, Ctrl+S, показ заставки
* после Restart Game) pop_mus_left не убывает НИКОГДА. Вызывающий,
* который ждёт конца мелодии, в этом состоянии висит вечно — ровно так
* заставка после Restart Game вставала на экране Prince of Persia
* (поймано пользователем 2026-09-02). Корень того случая был в другом
* (CBL не открывался заново, см. pop_boot_sound), но ждать неиграющую
* музыку нельзя в принципе, поэтому признак спрашивает и про вывод. */
return (uint8_t)(pop_snd_ok && pop_mus_left != 0);
}
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
+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;
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);
}
+22 -4
View File
@@ -21,10 +21,28 @@
#include <stdint.h>
/* Поднять набор: занять 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 уходит в диск надолго, насос не успевает долить
+54 -13
View File
@@ -18,6 +18,11 @@
#include <sprite.h> /* 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;
}
+44 -10
View File
@@ -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;
}
+135 -59
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
{
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
@@ -1141,77 +1171,114 @@ int pop_boot(void) __banked
gfx_set_visible_page(0);
pop_pal_black();
/* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после
* initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к
* этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */
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 страниц оцифровки (вывод пока молчит) */
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
* будет (pop_music_info вернёт -1 на любой трек), но игра работает:
* это необязательный ресурс, как и сами треки. */
(void)pop_music_index_load();
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed");
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) */
/* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной
* палитре и поэтому не открывает игроку старое содержимое VRAM. */
/* 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;
}
/* 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: его фон по контракту является игровой сценой. */
pop_current_level = FIRST_LEVEL;
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
pop_guard_reset();
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) {
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 */
}
/* ОТКРЫТИЕ CBL — ВНЕ РАЗОВОГО ГАРДА, и это не мелочь. Загрузка нужна
* один раз за запуск, а вот вывод к моменту следующего показа заставки
* почти наверняка закрыт: pop_title_show начинается с pop_sfx_pause, да
* и уровень оставляет CBL закрытым. Пока start стоял под флагом,
* Restart Game приводил к заставке БЕЗ музыки (поймано пользователем
* 2026-09-02), а заодно подвешивал ожидания «дослушать тему»: с
* выключенным насосом pop_mus_left не убывает, и pop_music_busy()
* истинен вечно. Идемпотентно и уважает Ctrl+S (pop_snd_want). */
(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;
}
boot_music_step();
/* Стартовая комната до 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 +1291,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 +1440,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();
+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_sound(void) __banked;
int pop_boot_rest(void) __banked;
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
+4 -4
View File
@@ -15,9 +15,9 @@ bios_text 4461
blitperf 5860
blitw 4939
cat 913
cblstream 5860
cbltest 6006
cblwav 6099
cblstream 5862
cbltest 6008
cblwav 6101
conio 4605
conio2 3929
convbench 3508
@@ -28,7 +28,7 @@ fdmax 6023
filetest 10547
fpsdiv 4870
gets 509
gfx_dbuf 5131
gfx_dbuf 5142
gfx_demo 4114
gfxbanks 6044
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) — общий счётчик желающих
* держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL-
* сессии. Нужно gfx_wait_vsync() (см. libc/gfx/gfx_wait_vsync.c): бит
* 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp kbd_fe_r),
* но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный CBL уже
* открыт — портом владеет он, ref/unref его не трогают; иначе включают
* bit7 "немым" кодом частоты _CBL_VSYNC_FREQ (таймер CBL не заводится
* — divs[2]=0 в MAME — ни звука, ни прерываний, только синхро-бит).
* сессии. Нужно gfx_wait_vsync() (см. libbgi/common/gfx_wait_vsync.c):
* бит 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp
* kbd_fe_r), но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный
* CBL уже открыт — портом владеет он, ref/unref его не трогают; иначе
* включают bit7 "немым" кодом частоты _CBL_VSYNC_FREQ и заливают
* аппаратный буфер тишиной — БЕЗ ЗАЛИВКИ ЖЕЛЕЗО ПОЁТ (см. ниже).
*
* ЖЕЛЕЗО ПОЁТ ТО, ЧТО ЛЕЖИТ В БУФЕРЕ. Буфер 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
#define _CBL_INTERNAL_H
@@ -42,7 +59,10 @@
#define _CBL_CTRL_PORT 0x004E
#define _CBL_DATA_PORT 0x4F
#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_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);
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше.
* Подробный разбор — в шапке _cbl_prime.c. */
* ЛЮБОЙ записи в порт управления: при включении — иначе первые 23 мс
* играет то, что лежало в буфере раньше; при выключении — иначе железо
* крутит хвост последнего сэмпла бесконечной нотой. Подробный разбор —
* в шапке _cbl_prime.c. */
void _cbl_prime(uint8_t silence);
/* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один
+20 -3
View File
@@ -7,9 +7,23 @@
*
* Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет
* он, ref/unref его не трогают вообще (это уже нужное состояние).
* Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ
* — таймер CBL при этом коде не заводится (divs[2]=0 в MAME), значит
* ни звука, ни прерываний, только доступ к синхро-биту.
* Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ.
*
* ТИШИНУ ДАЁТ ЗАЛИТЫЙ БУФЕР, А НЕ ОСТАНОВЛЕННЫЙ ТАЙМЕР. Раньше здесь
* стоял 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"
@@ -19,6 +33,9 @@ void _cbl_port_sync(void)
{
if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */
_cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0);
/* Формат "немого" режима всегда 8-битный (bit5 не выставлен), поэтому
* тишина здесь — 0x80 независимо от того, каким был закрытый сеанс. */
_cbl_prime(0x80);
}
void _cbl_port_ref(void)
+15 -1
View File
@@ -1,5 +1,6 @@
/*
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после включения.
* _cbl_prime — залить аппаратный буфер CBL тишиной СРАЗУ после записи в
* порт управления: и при включении, и при выключении.
*
* ЗАЧЕМ. Буфер CBL (256 слотов, две половины по 128) железо НЕ чистит ни
* при сбросе, ни при записи в порт управления — там остаётся то, что лежало
@@ -31,6 +32,19 @@
* Слотов ровно 256 НЕЗАВИСИМО от формата (в 16-бит слот держит целый
* сэмпл), поэтому и записей всегда 256 — байт тишины разный: 0x80 для
* 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"
+11 -2
View File
@@ -10,7 +10,16 @@
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
* ссылку (_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"
@@ -21,7 +30,7 @@ void cbl_close(void)
if (!_irq_cbl_hook) return;
IRQ_DISABLE();
_irq_cbl_hook = 0;
_cbl_port_sync(); /* режим порта + заливка буфера тишиной */
IRQ_ENABLE();
_cbl_port_sync();
_irq_table_unref();
}