Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e |
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
}
|
||||
|
||||
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 уходит в диск надолго, насос не успевает долить
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||
#include <cbl.h> /* cbl_shutdown — погасить железо звука на выходе */
|
||||
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||
@@ -1114,6 +1115,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 +1172,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 +1292,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 +1441,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();
|
||||
@@ -1414,4 +1491,14 @@ void pop_shutdown(void) __banked
|
||||
pop_guard_free();
|
||||
pop_kid_data_free();
|
||||
pop_level_free();
|
||||
/* ЖЕЛЕЗО ЗВУКА ГАСИМ ПОСЛЕДНИМ ДЕЙСТВИЕМ, и pop_sfx_close выше для этого
|
||||
* НЕ ДОСТАТОЧНО: он закрывает сессию, а bit7 порта CBL остаётся включён,
|
||||
* пока его держит gfx_wait_vsync ради бита луча. Дальше main просто
|
||||
* возвращается, а crt0 при возврате уходит прямо в ESTEX EXIT, минуя
|
||||
* цепочку atexit (runtime/crt0_banked.s) — то есть vsync-ссылку снять
|
||||
* уже некому. В результате мы уходили в шелл с включённым CBL, и он
|
||||
* крутил свои 256 слотов: тихо до первой чужой записи в порт данных, а
|
||||
* дальше она зацикливалась (поймано пользователем на железе 2026-09-02;
|
||||
* в MAME не воспроизводится). */
|
||||
cbl_shutdown();
|
||||
}
|
||||
|
||||
@@ -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 обязан
|
||||
|
||||
@@ -125,6 +125,8 @@ def find_ci(directory, name):
|
||||
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
||||
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
||||
GEN = APP / "gen" # генерируемые заголовки
|
||||
DIST = APP / "dist" # ручное содержимое дистрибутива (README и
|
||||
# производные наборы, напр. тихий звук)
|
||||
|
||||
|
||||
def atl(name):
|
||||
@@ -139,3 +141,10 @@ def packed(name):
|
||||
p = PACKED / name
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def dist(name):
|
||||
"""Каталог внутри dist/ в раскладке диска; создаётся при обращении."""
|
||||
p = DIST / name
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pop_quiet.py — тише в N раз: пережать ГОТОВЫЕ наборы звука и музыки.
|
||||
|
||||
Берёт то, что уже лежит в assets/packed (SND/snd.arc + MUS/m*.bin), делает
|
||||
тише и кладёт в dist/ в той же раскладке диска, ПЕРЕЗАПИСЫВАЯ то, что там
|
||||
было. Оригиналы в assets/packed не трогаются никогда — значит скрипт
|
||||
идемпотентен: сколько раз ни запусти, громкость упадёт ровно один раз.
|
||||
|
||||
ФОРМУЛА. PCM у нас 8 бит БЕЗ ЗНАКА, тишина = 0x80 (см. cbl.h и
|
||||
docs/sound_plan.md), то есть байт — это знаковое отклонение от 0x80.
|
||||
Тише вдвое:
|
||||
|
||||
out = 0x80 + (in - 0x80) / 2 = in / 2 + 0x40
|
||||
|
||||
Правая форма — та самая, что делается на Z80 двумя командами
|
||||
(`srl a` / `add a,#0x40`), поэтому таблицу строим floor-делением: тогда
|
||||
результат совпадает с ней БАЙТ В БАЙТ и его можно сверять с железной
|
||||
реализацией. Тишина остаётся тишиной (0x80 -> 0x80) при любом gain, а
|
||||
клиппинга не бывает по построению: |отклонение| только уменьшается.
|
||||
|
||||
ЧТО ИМЕННО ПЕРЕСЧИТЫВАЕТСЯ.
|
||||
|
||||
MUS/m*.bin — файл целиком: это чистый PCM без заголовка, загрузчик
|
||||
читает его страницами с нулевого смещения (pop_music.c).
|
||||
SND/snd.arc — ТОЛЬКО тела записей архива PBA1. Заголовок (512 Б) и
|
||||
выравнивающий хвост между записями остаются как есть:
|
||||
там нули упаковщика, а не звук, и пересчёт превратил бы
|
||||
их в 0x40 — мусор на месте служебных байт.
|
||||
*.idx — копируются без изменений: длины и смещения не поменялись,
|
||||
меняются только сами отсчёты. Магию всё же проверяем,
|
||||
чтобы молча не скопировать файл чужого формата.
|
||||
|
||||
Запуск:
|
||||
|
||||
python3 tools/pop_quiet.py # вдвое тише, packed -> dist
|
||||
python3 tools/pop_quiet.py --gain 0.25 # вчетверо тише
|
||||
python3 tools/pop_quiet.py --src DIR --out DIR
|
||||
"""
|
||||
import argparse
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import paths
|
||||
|
||||
SILENCE = 0x80 # центр 8-битного беззнакового PCM
|
||||
ARC_MAGIC = b"PBA1" # архив ресурсов, см. src/pop_arc.h
|
||||
ARC_HEADER = 512
|
||||
ARC_SECTOR = 512
|
||||
SND_IDX_MAGIC = b"PSI1"
|
||||
MUS_IDX_MAGIC = b"PMI1"
|
||||
|
||||
|
||||
def gain_table(gain: float) -> bytes:
|
||||
"""Таблица перекодировки байта: 256 значений, готовые для bytes.translate.
|
||||
|
||||
floor, а не округление к ближайшему: при gain = 1/2 это ровно
|
||||
`in >> 1 + 0x40`, то есть побайтово то же, что сделал бы Z80.
|
||||
"""
|
||||
if not 0.0 <= gain <= 1.0:
|
||||
raise SystemExit("gain обязан лежать в [0, 1]; громче делать нечем — "
|
||||
"запас по амплитуде в исходнике уже выбран")
|
||||
out = bytearray(256)
|
||||
for v in range(256):
|
||||
dev = v - SILENCE # знаковое отклонение
|
||||
out[v] = max(0, min(255, SILENCE + _floor_div(dev, gain)))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _floor_div(dev: int, gain: float) -> int:
|
||||
"""dev * gain, округлённое ВНИЗ (к минус бесконечности)."""
|
||||
scaled = dev * gain
|
||||
i = int(scaled)
|
||||
return i - 1 if scaled < i else i # int() режет к нулю
|
||||
|
||||
|
||||
def quiet_raw(src: Path, dst: Path, table: bytes) -> int:
|
||||
"""Файл целиком — PCM (музыкальный трек). Возвращает размер."""
|
||||
data = src.read_bytes()
|
||||
dst.write_bytes(data.translate(table))
|
||||
return len(data)
|
||||
|
||||
|
||||
def quiet_arc(src: Path, dst: Path, table: bytes) -> int:
|
||||
"""Архив PBA1: пересчитываем только тела записей."""
|
||||
raw = bytearray(src.read_bytes())
|
||||
if raw[0:4] != ARC_MAGIC:
|
||||
raise SystemExit(f"{src}: не {ARC_MAGIC.decode()} — это не наш архив")
|
||||
count = raw[4]
|
||||
if not 1 <= count <= (ARC_HEADER - 8) // 4:
|
||||
raise SystemExit(f"{src}: count = {count}, вне 1..126")
|
||||
for i in range(count):
|
||||
off_sec, size = struct.unpack_from("<HH", raw, 8 + i * 4)
|
||||
off = off_sec * ARC_SECTOR
|
||||
if off + size > len(raw):
|
||||
raise SystemExit(f"{src}: запись {i} выходит за файл "
|
||||
f"({off} + {size} > {len(raw)})")
|
||||
raw[off:off + size] = bytes(raw[off:off + size]).translate(table)
|
||||
dst.write_bytes(bytes(raw))
|
||||
return count
|
||||
|
||||
|
||||
def copy_idx(src: Path, dst: Path, magic: bytes) -> None:
|
||||
"""Индекс переносим как есть — но убеждаемся, что он наш."""
|
||||
head = src.read_bytes()[:4]
|
||||
if head != magic:
|
||||
raise SystemExit(f"{src}: магия {head!r}, ожидалась {magic!r}")
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
|
||||
def peak(data: bytes) -> int:
|
||||
"""Максимальное отклонение от тишины — для отчёта «стало тише»."""
|
||||
return max(abs(b - SILENCE) for b in data) if data else 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--gain", type=float, default=0.5,
|
||||
help="множитель амплитуды (по умолчанию 0.5 — вдвое тише)")
|
||||
ap.add_argument("--src", type=Path, default=paths.PACKED,
|
||||
help="откуда брать готовые наборы (умолчание assets/packed)")
|
||||
ap.add_argument("--out", type=Path, default=paths.DIST,
|
||||
help="куда класть (умолчание dist/)")
|
||||
args = ap.parse_args()
|
||||
|
||||
table = gain_table(args.gain)
|
||||
src_snd, src_mus = args.src / "SND", args.src / "MUS"
|
||||
out_snd, out_mus = args.out / "SND", args.out / "MUS"
|
||||
if not src_snd.is_dir() or not src_mus.is_dir():
|
||||
raise SystemExit(f"{args.src}: нет SND/ или MUS/ — сначала "
|
||||
"`make resources`")
|
||||
out_snd.mkdir(parents=True, exist_ok=True)
|
||||
out_mus.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"тише в {1 / args.gain:g} раз(а): {args.src} -> {args.out}")
|
||||
|
||||
arc = src_snd / "snd.arc"
|
||||
before = peak(arc.read_bytes()[ARC_HEADER:])
|
||||
n = quiet_arc(arc, out_snd / "snd.arc", table)
|
||||
after = peak((out_snd / "snd.arc").read_bytes()[ARC_HEADER:])
|
||||
copy_idx(src_snd / "snd.idx", out_snd / "snd.idx", SND_IDX_MAGIC)
|
||||
print(f" SND/snd.arc {n} записей, пик {before} -> {after}")
|
||||
|
||||
tracks = sorted(src_mus.glob("m*.bin"))
|
||||
if not tracks:
|
||||
raise SystemExit(f"{src_mus}: нет ни одного m*.bin")
|
||||
total = 0
|
||||
worst_before = worst_after = 0
|
||||
for t in tracks:
|
||||
total += quiet_raw(t, out_mus / t.name, table)
|
||||
worst_before = max(worst_before, peak(t.read_bytes()))
|
||||
worst_after = max(worst_after, peak((out_mus / t.name).read_bytes()))
|
||||
copy_idx(src_mus / "mus.idx", out_mus / "mus.idx", MUS_IDX_MAGIC)
|
||||
print(f" MUS/ {len(tracks)} треков, {total // 1024} КБ, "
|
||||
f"пик {worst_before} -> {worst_after}")
|
||||
print("индексы (snd.idx, mus.idx) скопированы без изменений — длины те же")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,108 @@
|
||||
"""test_pop_quiet — правило громкости и неприкосновенность служебных байт.
|
||||
|
||||
Скрипт трогает данные, которые потом играет насос на Z80, поэтому тест
|
||||
закрепляет ровно две вещи: формулу (она обязана совпадать с беззнаковым
|
||||
`A/2 + 0x40`, чтобы результат можно было сверять с железной реализацией) и
|
||||
то, что заголовок архива с выравнивающим хвостом остаются как были.
|
||||
|
||||
Запуск: make test-tools (либо python3 -m unittest discover tools/tests)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import pop_quiet as X
|
||||
|
||||
|
||||
class Formula(unittest.TestCase):
|
||||
"""Таблица громкости: то же, что сделал бы Z80."""
|
||||
|
||||
def test_half_is_shift_plus_40(self):
|
||||
t = X.gain_table(0.5)
|
||||
for v in range(256):
|
||||
self.assertEqual(t[v], (v >> 1) + 0x40, f"байт {v:#04x}")
|
||||
|
||||
def test_silence_stays_silence(self):
|
||||
for gain in (0.5, 0.25, 0.1, 1.0):
|
||||
self.assertEqual(X.gain_table(gain)[X.SILENCE], X.SILENCE)
|
||||
|
||||
def test_no_clipping_and_range_shrinks(self):
|
||||
t = X.gain_table(0.5)
|
||||
self.assertEqual(t[0x00], 0x40)
|
||||
self.assertEqual(t[0xFF], 0xBF)
|
||||
for v in range(256):
|
||||
self.assertLessEqual(abs(t[v] - X.SILENCE), abs(v - X.SILENCE))
|
||||
|
||||
def test_gain_one_is_identity(self):
|
||||
self.assertEqual(X.gain_table(1.0), bytes(range(256)))
|
||||
|
||||
def test_gain_out_of_range_rejected(self):
|
||||
for bad in (-0.1, 1.5):
|
||||
with self.assertRaises(SystemExit):
|
||||
X.gain_table(bad)
|
||||
|
||||
|
||||
def make_arc(bodies):
|
||||
"""Собрать PBA1 ровно так, как это делает tools/pop_pack_arc.py."""
|
||||
raw = bytearray(X.ARC_HEADER)
|
||||
raw[0:4] = X.ARC_MAGIC
|
||||
raw[4] = len(bodies)
|
||||
blob = bytearray()
|
||||
for i, body in enumerate(bodies):
|
||||
off = X.ARC_HEADER + len(blob)
|
||||
struct.pack_into("<HH", raw, 8 + i * 4, off // X.ARC_SECTOR, len(body))
|
||||
blob += body
|
||||
blob += bytes(-len(blob) % X.ARC_SECTOR) # хвост выравнивания — нули
|
||||
return bytes(raw + blob)
|
||||
|
||||
|
||||
class Archive(unittest.TestCase):
|
||||
"""PBA1: тела пересчитаны, служебные байты — нет."""
|
||||
|
||||
def setUp(self):
|
||||
self.dir = Path(tempfile.mkdtemp())
|
||||
self.bodies = [bytes([0x80, 0xFF, 0x00, 0x81, 0x7F]), bytes([0xC0] * 3)]
|
||||
self.src = self.dir / "in.arc"
|
||||
self.dst = self.dir / "out.arc"
|
||||
self.src.write_bytes(make_arc(self.bodies))
|
||||
|
||||
def test_bodies_scaled(self):
|
||||
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||
raw = self.dst.read_bytes()
|
||||
off = X.ARC_HEADER
|
||||
got = raw[off:off + len(self.bodies[0])]
|
||||
self.assertEqual(got, bytes([0x80, 0xBF, 0x40, 0x80, 0x7F]))
|
||||
|
||||
def test_header_and_padding_untouched(self):
|
||||
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||
src, dst = self.src.read_bytes(), self.dst.read_bytes()
|
||||
self.assertEqual(len(src), len(dst))
|
||||
self.assertEqual(src[:X.ARC_HEADER], dst[:X.ARC_HEADER])
|
||||
pad_at = X.ARC_HEADER + len(self.bodies[0])
|
||||
pad_len = -len(self.bodies[0]) % X.ARC_SECTOR
|
||||
self.assertEqual(dst[pad_at:pad_at + pad_len], bytes(pad_len))
|
||||
|
||||
def test_foreign_magic_rejected(self):
|
||||
self.src.write_bytes(b"NOPE" + bytes(X.ARC_HEADER))
|
||||
with self.assertRaises(SystemExit):
|
||||
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||
|
||||
|
||||
class Track(unittest.TestCase):
|
||||
"""Музыкальный трек — чистый PCM, пересчитывается целиком."""
|
||||
|
||||
def test_whole_file(self):
|
||||
d = Path(tempfile.mkdtemp())
|
||||
src, dst = d / "m54.bin", d / "out.bin"
|
||||
src.write_bytes(bytes([0x00, 0x80, 0xFF] * 4))
|
||||
n = X.quiet_raw(src, dst, X.gain_table(0.5))
|
||||
self.assertEqual(n, 12)
|
||||
self.assertEqual(dst.read_bytes(), bytes([0x40, 0x80, 0xBF] * 4))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+45
-45
@@ -1,67 +1,67 @@
|
||||
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
||||
accfill 3783
|
||||
accfill 3797
|
||||
accop 7067
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
argv 3445
|
||||
assrtest 3861
|
||||
atlas 9193
|
||||
attrprob 4087
|
||||
banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
banktest 3767
|
||||
bgi_img 8677
|
||||
bgitest 3748
|
||||
bios_text 4461
|
||||
attrprob 4101
|
||||
banked 1070
|
||||
bankedbg 1081
|
||||
banklocl 4697
|
||||
banktest 3781
|
||||
bgi_img 8691
|
||||
bgitest 3762
|
||||
bios_text 4475
|
||||
blitperf 5860
|
||||
blitw 4939
|
||||
cat 913
|
||||
cblstream 5860
|
||||
cbltest 6006
|
||||
cblwav 6099
|
||||
conio 4605
|
||||
conio2 3929
|
||||
cat 927
|
||||
cblstream 5944
|
||||
cbltest 6090
|
||||
cblwav 6183
|
||||
conio 4619
|
||||
conio2 3943
|
||||
convbench 3508
|
||||
dec_test 860
|
||||
errno 5966
|
||||
fbench 8085
|
||||
fdmax 6023
|
||||
filetest 10547
|
||||
dec_test 874
|
||||
errno 5980
|
||||
fbench 8149
|
||||
fdmax 6087
|
||||
filetest 10611
|
||||
fpsdiv 4870
|
||||
gets 509
|
||||
gfx_dbuf 5131
|
||||
gfx_demo 4114
|
||||
gets 523
|
||||
gfx_dbuf 5207
|
||||
gfx_demo 4128
|
||||
gfxbanks 6044
|
||||
hello 4167
|
||||
hello2 4244
|
||||
irqtest 6690
|
||||
hello 4181
|
||||
hello2 4258
|
||||
irqtest 6754
|
||||
kbdpoll 1282
|
||||
kbdraw 4911
|
||||
ls 4835
|
||||
malloc 4461
|
||||
mem_test 4555
|
||||
mouse 4382
|
||||
openenv 6124
|
||||
ls 4849
|
||||
malloc 4475
|
||||
mem_test 4569
|
||||
mouse 4396
|
||||
openenv 6138
|
||||
pageflip 6334
|
||||
palfile 5406
|
||||
ptime 5744
|
||||
rt_test 5197
|
||||
ptime 5758
|
||||
rt_test 5216
|
||||
scroll 6744
|
||||
seek 4188
|
||||
simple 955
|
||||
solidt 11531
|
||||
seek 4202
|
||||
simple 969
|
||||
solidt 11595
|
||||
spranim 9431
|
||||
spriteclip 4119
|
||||
sprites 6791
|
||||
stattest 7549
|
||||
stdlib 6643
|
||||
stest2 3644
|
||||
strtest 1340
|
||||
text_palette 5024
|
||||
timedir 5454
|
||||
stattest 7563
|
||||
stdlib 6657
|
||||
stest2 3658
|
||||
strtest 1354
|
||||
text_palette 5038
|
||||
timedir 5468
|
||||
w0page 8831
|
||||
w3bgfx 5243
|
||||
w3big 3673
|
||||
w3huge 3700
|
||||
w3probe 3556
|
||||
w3tiny 3471
|
||||
winrest 4459
|
||||
winrest 4473
|
||||
|
||||
|
+31
-9
@@ -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(), при недоливе — тишина по режиму (один
|
||||
|
||||
@@ -61,7 +61,12 @@ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||
static uint8_t atexit_armed;
|
||||
if (!atexit_armed) {
|
||||
atexit_armed = 1;
|
||||
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
|
||||
/* ИМЕННО shutdown, а не close: close оставляет bit7 включённым,
|
||||
* пока его держит gfx_wait_vsync, и железо крутит буфер уже под
|
||||
* шеллом. Подстраховка неполная — цепочку atexit разматывает
|
||||
* только exit(), а crt0 при возврате из main уходит прямо в ESTEX
|
||||
* EXIT, — поэтому программа обязана звать cbl_shutdown() сама. */
|
||||
atexit(cbl_shutdown); /* CBL обязан умолкнуть до шелла */
|
||||
}
|
||||
|
||||
IRQ_DISABLE();
|
||||
|
||||
+20
-3
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* cbl_shutdown — ЖЁСТКО погасить звук перед возвратом в шелл.
|
||||
*
|
||||
* ЧЕМ ОТЛИЧАЕТСЯ ОТ cbl_close(). Тот закрывает СЕССИЮ: снимает насос и
|
||||
* пересчитывает порт по текущим держателям. А держатель обычно есть —
|
||||
* gfx_wait_vsync() занимает bit7 навсегда ради бита луча (см. _cbl_port.c),
|
||||
* — и порт остаётся включённым в "немом" режиме. Для работающей программы
|
||||
* это правильно, но при выходе означает, что железо продолжает крутить свои
|
||||
* 256 слотов уже под шеллом: тихо ровно до первой чужой записи в порт
|
||||
* данных, а дальше эта запись зацикливается навсегда.
|
||||
*
|
||||
* ПОЧЕМУ НЕЛЬЗЯ ПОЛОЖИТЬСЯ НА atexit. Цепочку atexit разматывает только
|
||||
* exit() (libc/sys/exit.c), а crt0 при ВОЗВРАТЕ ИЗ main уходит прямо в
|
||||
* ESTEX EXIT (runtime/crt0_banked.s) — ни один хендлер не выполняется.
|
||||
* Значит и снятие vsync-ссылки, и cbl_close из atexit при обычном
|
||||
* `return 0;` из main не происходят. Поэтому выключение должно быть
|
||||
* ЯВНЫМ действием программы, а не следствием чужой уборки.
|
||||
*
|
||||
* Что делает: закрывает сессию (если была), обнуляет счётчик держателей —
|
||||
* программа уходит, держать порт больше некому, — гасит bit7 и центрует
|
||||
* ЦАП обычного COVOX (при выключенном CBL записи в порт данных идут прямо
|
||||
* в него, см. _cbl_prime.c), чтобы вместо щелчка осталась ровная тишина.
|
||||
*
|
||||
* Идемпотентно; звать последним действием завершения.
|
||||
*/
|
||||
|
||||
#include "_cbl.h"
|
||||
#include "../irq/_irq.h" /* IRQ_DISABLE/IRQ_ENABLE */
|
||||
|
||||
void cbl_shutdown(void)
|
||||
{
|
||||
cbl_close(); /* снять насос и отпустить IM2-таблицу */
|
||||
IRQ_DISABLE();
|
||||
_cbl_port_refs = 0; /* держателей больше нет: программа уходит */
|
||||
_cbl_ctrl(0); /* bit7 = 0 — CBL выключен целиком */
|
||||
_cbl_prime(0x80); /* ...и ЦАП COVOX в центре, без щелчка */
|
||||
IRQ_ENABLE();
|
||||
}
|
||||
+13
-3
@@ -55,9 +55,14 @@
|
||||
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
||||
* поведение не меняет.
|
||||
*
|
||||
* cbl_close() обязателен (atexit подстрахует) — иначе CBL продолжит
|
||||
* прерывать шелл после выхода; заодно освобождает буфер тишины, если
|
||||
* он был аллоцирован.
|
||||
* cbl_close() обязателен — иначе CBL продолжит прерывать шелл после
|
||||
* выхода. НО ОН НЕ ГАСИТ ЖЕЛЕЗО ПОЛНОСТЬЮ: пока bit7 держит
|
||||
* gfx_wait_vsync() ради бита луча, порт остаётся включённым в "немом"
|
||||
* режиме, и буфер крутится под шеллом. Перед возвратом из main звать
|
||||
* cbl_shutdown() — он выключает всё и центрует ЦАП.
|
||||
*
|
||||
* На atexit тут полагаться НЕЛЬЗЯ: цепочку разматывает только exit(), а
|
||||
* crt0 при возврате из main уходит прямо в ESTEX EXIT.
|
||||
*
|
||||
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
|
||||
*/
|
||||
@@ -67,6 +72,11 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Выключить звук ПОЛНОСТЬЮ перед завершением программы: закрыть сессию,
|
||||
* снять bit7 независимо от держателей и оставить ЦАП в центре. Звать
|
||||
* последним действием завершения; идемпотентно. */
|
||||
void cbl_shutdown(void);
|
||||
|
||||
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
|
||||
* Sprinter Team; коды 0/1 — legacy, не использовать). */
|
||||
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
|
||||
|
||||
@@ -10,4 +10,11 @@
|
||||
extern void (*_atexit_stack[ATEXIT_MAX])(void);
|
||||
extern int _atexit_top;
|
||||
|
||||
/* Разматыватель цепочки (LIFO) — общее тело exit() и возврата из main. */
|
||||
void _atexit_run(void);
|
||||
|
||||
/* Указатель на него для crt0; ставит atexit() при первой регистрации.
|
||||
* Подробности и мотивация косвенности — в шапке _atexit_hook.c. */
|
||||
extern void (*_atexit_hook)(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* _atexit_hook — указатель на разматыватель цепочки atexit; его дёргает
|
||||
* crt0 при ВОЗВРАТЕ ИЗ main.
|
||||
*
|
||||
* ЗАЧЕМ КОСВЕННОСТЬ. По стандарту C возврат из main эквивалентен
|
||||
* exit(status), то есть цепочка atexit обязана размотаться. Но прямая
|
||||
* ссылка crt0 на разматыватель притащила бы в КАЖДУЮ программу и его, и
|
||||
* весь стек хендлеров — включая те, что atexit() не зовут вовсе. Поэтому
|
||||
* crt0 ссылается на этот модуль (два байта _DATA, ни байта кода), а
|
||||
* заполняет указатель сам atexit() при первой регистрации: нет
|
||||
* регистраций — указатель нулевой, разматыватель в программу не приезжает,
|
||||
* crt0 просто уходит в ESTEX EXIT как раньше. Тот же приём, что у
|
||||
* _irq_cbl_hook в libc/irq.
|
||||
*/
|
||||
|
||||
#include "_atexit.h"
|
||||
|
||||
void (*_atexit_hook)(void);
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* _atexit_run — размотать цепочку atexit-хендлеров (LIFO).
|
||||
*
|
||||
* Общее тело для двух входов: явного exit() и возврата из main (там его
|
||||
* зовёт crt0 через _atexit_hook). Отдельным модулем — чтобы программа,
|
||||
* не зарегистрировавшая ни одного хендлера, не получала этот код вовсе.
|
||||
*
|
||||
* Вершина сбрасывается ПЕРЕД вызовом каждого хендлера: если хендлер сам
|
||||
* позовёт exit(), цепочка не пойдёт по второму кругу.
|
||||
*/
|
||||
|
||||
#include "_atexit.h"
|
||||
|
||||
void _atexit_run(void)
|
||||
{
|
||||
while (_atexit_top > 0) {
|
||||
void (*fn)(void) = _atexit_stack[--_atexit_top];
|
||||
if (fn) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -3,9 +3,11 @@
|
||||
* завершении (exit()), максимум 8, порядок LIFO. Возвращает 0 или -1
|
||||
* при переполнении стека хендлеров.
|
||||
*
|
||||
* Внимание: возврат из main() без явного exit() уходит в inline RST
|
||||
* 10h #41 в crt0.s и цепочку atexit НЕ выполняет — программам с
|
||||
* хендлерами нужно завершаться через exit().
|
||||
* ВОЗВРАТ ИЗ main ТОЖЕ РАЗМАТЫВАЕТ ЦЕПОЧКУ (с 2026-09-02), как и требует
|
||||
* стандарт C. Раньше crt0 уходил из main прямо в ESTEX EXIT, и хендлеры
|
||||
* не выполнялись: программа теряла и сброс файловых буферов (_fclosall), и
|
||||
* гашение звука, если не звала exit() явно. Теперь первая регистрация
|
||||
* ставит _atexit_hook, а crt0 его дёргает — см. _atexit_hook.c.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
@@ -17,5 +19,8 @@ int atexit(void (*fn)(void))
|
||||
return -1;
|
||||
}
|
||||
_atexit_stack[_atexit_top++] = fn;
|
||||
/* Хендлеры появились — значит crt0 обязан размотать цепочку и при
|
||||
* простом `return` из main. Ставим каждый раз: проверка дороже. */
|
||||
_atexit_hook = _atexit_run;
|
||||
return 0;
|
||||
}
|
||||
|
||||
+1
-6
@@ -9,11 +9,6 @@
|
||||
|
||||
void exit(int code)
|
||||
{
|
||||
while (_atexit_top > 0) {
|
||||
void (*fn)(void) = _atexit_stack[--_atexit_top];
|
||||
if (fn) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
_atexit_run();
|
||||
_exit(code);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
.module crt0
|
||||
.globl _main
|
||||
.globl __atexit_hook
|
||||
|
||||
;; Linker-emitted symbols (resolved at link time).
|
||||
.globl s__INITIALIZER
|
||||
@@ -88,6 +89,22 @@ _start::
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; Returning from main is exit(status) per the C standard, so the
|
||||
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||
;; that never register anything keep it NULL and link neither the
|
||||
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||
push de ; DE = int return of main
|
||||
ld hl, (__atexit_hook)
|
||||
ld a, h
|
||||
or a, l
|
||||
jr Z, atexit_done
|
||||
ld de, #atexit_done
|
||||
push de ; return address: Z80 has no `call (hl)`
|
||||
jp (hl)
|
||||
atexit_done:
|
||||
pop de
|
||||
|
||||
;; SDCC's int return → DE. Low byte is the exit code.
|
||||
ld a, e
|
||||
ld b, a
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
.module crt0_banked
|
||||
.globl _main
|
||||
.globl __atexit_hook
|
||||
.globl _n_banks
|
||||
.globl _bank_pages
|
||||
|
||||
@@ -218,6 +219,22 @@ skip_bank_load:
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; Returning from main is exit(status) per the C standard, so the
|
||||
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||
;; that never register anything keep it NULL and link neither the
|
||||
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||
push de ; DE = int return of main
|
||||
ld hl, (__atexit_hook)
|
||||
ld a, h
|
||||
or a, l
|
||||
jr Z, atexit_done
|
||||
ld de, #atexit_done
|
||||
push de ; return address: Z80 has no `call (hl)`
|
||||
jp (hl)
|
||||
atexit_done:
|
||||
pop de
|
||||
|
||||
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
|
||||
ld a, e
|
||||
jr exit_with_a
|
||||
|
||||
Reference in New Issue
Block a user