diff --git a/applications/SprPoP/src/sprpop_cold.c b/applications/SprPoP/src/sprpop_cold.c index 15be564..827e915 100644 --- a/applications/SprPoP/src/sprpop_cold.c +++ b/applications/SprPoP/src/sprpop_cold.c @@ -25,6 +25,7 @@ #include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */ #include "pop_shadow.h" /* запечённый набор Тени */ #include "pop_sfx.h" /* звуковые эффекты через CBL */ +#include /* cbl_shutdown — погасить железо звука на выходе */ #include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */ #include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */ #include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */ @@ -1490,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(); } diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index ce69095..97b978a 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -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 5862 -cbltest 6008 -cblwav 6101 -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 5142 -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 diff --git a/libc/cbl/_cbl_open_raw.c b/libc/cbl/_cbl_open_raw.c index 690b872..6ab85df 100644 --- a/libc/cbl/_cbl_open_raw.c +++ b/libc/cbl/_cbl_open_raw.c @@ -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(); diff --git a/libc/cbl/cbl_shutdown.c b/libc/cbl/cbl_shutdown.c new file mode 100644 index 0000000..8569c59 --- /dev/null +++ b/libc/cbl/cbl_shutdown.c @@ -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(); +} diff --git a/libc/include/cbl.h b/libc/include/cbl.h index 3c23ea5..2ef5d7e 100644 --- a/libc/include/cbl.h +++ b/libc/include/cbl.h @@ -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. * * Требования как у : код/данные в W2 (tiny/big). */ @@ -67,6 +72,11 @@ #include +/* Выключить звук ПОЛНОСТЬЮ перед завершением программы: закрыть сессию, + * снять bit7 независимо от держателей и оставить ЦАП в центре. Звать + * последним действием завершения; идемпотентно. */ +void cbl_shutdown(void); + /* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума * Sprinter Team; коды 0/1 — legacy, не использовать). */ #define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */ diff --git a/libc/sys/_atexit.h b/libc/sys/_atexit.h index c70e9ec..14186bf 100644 --- a/libc/sys/_atexit.h +++ b/libc/sys/_atexit.h @@ -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 diff --git a/libc/sys/_atexit_hook.c b/libc/sys/_atexit_hook.c new file mode 100644 index 0000000..dbe2cf9 --- /dev/null +++ b/libc/sys/_atexit_hook.c @@ -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); diff --git a/libc/sys/_atexit_run.c b/libc/sys/_atexit_run.c new file mode 100644 index 0000000..54b80f5 --- /dev/null +++ b/libc/sys/_atexit_run.c @@ -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(); + } + } +} diff --git a/libc/sys/atexit.c b/libc/sys/atexit.c index ee4d7ba..9cb4eb6 100644 --- a/libc/sys/atexit.c +++ b/libc/sys/atexit.c @@ -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 @@ -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; } diff --git a/libc/sys/exit.c b/libc/sys/exit.c index a4c432b..7e71f14 100644 --- a/libc/sys/exit.c +++ b/libc/sys/exit.c @@ -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); } diff --git a/runtime/crt0.s b/runtime/crt0.s index 4146c4a..8992704 100644 --- a/runtime/crt0.s +++ b/runtime/crt0.s @@ -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 diff --git a/runtime/crt0_banked.s b/runtime/crt0_banked.s index 8f9364c..85a2d21 100644 --- a/runtime/crt0_banked.s +++ b/runtime/crt0_banked.s @@ -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