Ctrl+S — вкл/выкл звук; чит «выдать меч» убран

Порт Ctrl+S из SDLPoP (seg000:657).  Выключение реально закрывает CBL, а
не глушит сэмпл: иначе насос продолжал бы отдавать блоки тишины и платить
те же 3,26 % процессорного времени.

Флаг намерения pop_snd_want отдельно от pop_snd_ok: последний гасит
служебная пауза на время загрузки уровня, и правь Ctrl+S только его —
первая же смена уровня вернула бы выключенный звук.

Индикация: пурпурная палочка в борте, когда звука НЕТ (включённый слышно
и так, а молчание неотличимо от «нечему звучать»).

Чит S «выдать меч» был отладочным и больше не нужен — снят, клавиша ушла
под звук.

Замер цены звука — sound_plan.md §9: 8 001 такт на прерывание при периоде
245 759 (3,26 % времени), +2,2 % к работе кадра в сцене 11/15, период
кадра не сдвинулся ни разу.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 15:49:33 +03:00
parent 7e2e7fbb37
commit e0c86a96ef
7 changed files with 147 additions and 13 deletions
+70
View File
@@ -383,3 +383,73 @@ IBM PC 1989 года: один квадратный голос. Он у нас
DOS-версией; у нас последовательности разведены по доменам DOS-версией; у нас последовательности разведены по доменам
(`impl_diff.md`), поэтому у тряски свой сид, а холостой бросок не делаем — (`impl_diff.md`), поэтому у тряски свой сид, а холостой бросок не делаем —
на розыгрыши физики и кладки это не влияет. на розыгрыши физики и кладки это не влияет.
## 9. Цена звука в тактах (замер 2026-08-20, MAME)
Вопрос был поставлен так: звук идёт по прерываниям, значит размазан по всем
фазам кадра — и если фазы укладываются, всё хорошо? Да, но проверять это
надо не по фазам, а по двум числам, потому что **нагрузка от звука
постоянная и от сцены не зависит вовсе**.
### 9.1 Одно прерывание CBL
Зонды: `bpset` на входе трамплина (`_irq_tramp`) и на `reti` ветки CBL,
разница `totalcycles`. 398 замеров в сцене 11/15.
| величина | значение |
|---|---:|
| цена одного прерывания | **7 825 тактов ровно**, с разбросом до 8 299 (среднее 8 001) |
| период между прерываниями | 245 759 тактов (= 128 сэмплов на 10 937,5 Гц) |
| **доля процессорного времени** | **8 001 / 245 759 = 3,26 %** |
Цена постоянная, потому что работа фиксированная: OTIR ровно 128 байт плюс
скобка сохранения контекста. Ветвлений по данным в насосе нет.
### 9.2 Дрожание обслуживания — риск для ЗВУКА, не для кадра
Период плавает 228 804 … 262 734, то есть прерывание опаздывает максимум на
**~17 000 тактов = 0,8 мс**. Это самая длинная DI-скобка в коде
(акселератор режется по 16 строк, memory `sprinter_wait_states_2x`).
Буфер CBL — 128 сэмплов = **11,7 мс**, запас **14×**. Недолива быть не
может; счётчик `cbl_underruns()` это подтверждает косвенно (наш `fill`
всегда возвращает 1, поэтому он ловит только отсутствие данных, не
опоздание).
### 9.3 A/B в одном прогоне (Ctrl+S), сцена 11/15
Один и тот же кадр, звук выключается на ходу — сравнение чистое.
| | работа min | работа max | работа avg | прерываний CBL на кадр |
|---|---:|---:|---:|---:|
| звук ВКЛ | 453 132 | 572 532 | **498 064** | 1,40 |
| звук ВЫКЛ | 444 348 | 559 434 | **487 372** | 0,00 |
| разница | +8 784 | +13 098 | **+10 692 (+2,2 %)** | |
Разница на лёгком кадре (+8 784) — ровно одно прерывание, сходится с §9.1.
`CBL/кадр = 0` при выключенном звуке подтверждает, что Ctrl+S реально
ЗАКРЫВАЕТ CBL, а не глушит сэмпл: иначе насос продолжал бы отдавать блоки
тишины и платить те же 3,26 %.
### 9.4 Укладываемся ли
Логический кадр (`pop_pace.h`): NORMAL = 4 растра вне боя = **1 720 000
тактов**, FASTEST = 3 растра = **1 290 000**.
| | работа | доля NORMAL | доля FASTEST |
|---|---:|---:|---:|
| 11/15, обычная позиция | 498 064 | 29 % | 39 % |
| 11/15, тяжёлая позиция (Кид на 7 px правее) | 750 066 макс | 44 % | 58 % |
Период кадра за все прогоны: 1 719 936 … 1 720 752 — ровно 4 растра, ни
одного проскока. **Звук занимает 1,1 % бюджета NORMAL и 1,5 % FASTEST.**
### 9.5 Где 3,26 % МОГЛИ БЫ стоить дорого
Ответ «всё размазано, если фазы влезли — ок» верен с одной оговоркой.
Пейсинг квантован растром: работа 1,00 растра и 1,02 растра дают РАЗНЫЙ
период кадра (3 против 4 интервалов), то есть скачок сразу на 20 мс.
Значит звук опасен ровно в одной ситуации — когда сцена стоит в пределах
~8 000 тактов НИЖЕ кратного растру порога. Сейчас ближайший запас — 540 000
тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять
эту оговорку заново стоит только если работа кадра подберётся к 430 000 или
860 000 вплотную.
+3 -2
View File
@@ -60,8 +60,9 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр); дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии); **1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I** **ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I**
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень, бессмертие, **Shift+L** — следующий уровень,
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру **+/−** — обход комнат (`ROOMNAV`). Не чит, а штатная клавиша оригинала:
**Ctrl+S** — вкл/выкл звук (пурпурная палочка в борте = звука нет). Уровень грузится по номеру
(`pop_level_load_num`), на диске лежат все 15. (`pop_level_load_num`), на диске лежат все 15.
## Модули ## Модули
+3 -1
View File
@@ -22,7 +22,9 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида по кругу #define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида по кругу
* 0 -> 1 (только бой) -> 2 (и мелкий урон) -> 0; * 0 -> 1 (только бой) -> 2 (и мелкий урон) -> 0;
* уровни расписаны в pop_state.h */ * уровни расписаны в pop_state.h */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */ /* Ctrl+S (PS/2 set 2: S = 0x1B) — вкл/выкл звук; порт Ctrl+S из SDLPoP
* (seg000:657). С Ctrl, а не голая S: у оригинала клавиша именно такая. */
#define KBD_SOUND_TOGGLE 0x1B
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */ #define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */ #define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */ #define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
+12
View File
@@ -28,6 +28,18 @@
uint8_t pop_snd_page[POP_SND_PAGES]; uint8_t pop_snd_page[POP_SND_PAGES];
uint8_t pop_snd_ok; uint8_t pop_snd_ok;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
*
* Разделять обязательно: pop_snd_ok значит «CBL сейчас открыт», и его
* гасит служебная пауза на время загрузки уровня. Если бы Ctrl+S правил
* только его, то первая же смена уровня позвала бы pop_sfx_start() и
* вернула звук, который пользователь выключил. Поэтому start уважает
* этот флаг, а pause его не трогает.
*
* Живёт в резиденте, а не в банке: метку в борте рисует отладочный ярлык
* каждый кадр, и тащить ради одного байта трамплин незачем. */
uint8_t pop_snd_want;
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание. /* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory * Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на * avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
+11
View File
@@ -38,6 +38,17 @@ void pop_sfx_pause(void) __banked;
/* Закрыть CBL и отпустить страницы; звать на выходе. */ /* Закрыть CBL и отпустить страницы; звать на выходе. */
void pop_sfx_close(void) __banked; void pop_sfx_close(void) __banked;
/* Вкл/выкл звук по воле пользователя (Ctrl+S, порт SDLPoP seg000:657).
* Правит pop_snd_want и приводит вывод в соответствие. */
void pop_sfx_toggle(void) __banked;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ. Не то же самое, что «CBL открыт»: вывод
* ещё гасит служебная пауза на время загрузки уровня. Читать для
* индикации; писать — только через pop_sfx_toggle. 0 и в случае, когда
* набор вообще не поднялся (нет файлов, нет страниц) — то есть флаг
* честно means «звука не будет». */
extern uint8_t pop_snd_want;
/* Начать эффект с номером id (0..56 — нумерация оригинала, см. /* Начать эффект с номером id (0..56 — нумерация оригинала, см.
* sound_plan.md). Перебивает играющий. У звуков без оцифровки (это * sound_plan.md). Перебивает играющий. У звуков без оцифровки (это
* музыка) длина 0 — вызов просто глушит текущий. * музыка) длина 0 — вызов просто глушит текущий.
+23 -2
View File
@@ -32,6 +32,7 @@ int pop_sfx_init(void) __banked
} }
} }
snd_blk = blk; snd_blk = blk;
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
return 0; return 0;
} }
@@ -52,8 +53,9 @@ int pop_sfx_init(void) __banked
* резиденте W2 тесная, и её отказ ронял бы весь звук. */ * резиденте W2 тесная, и её отказ ронял бы весь звук. */
int pop_sfx_start(void) __banked int pop_sfx_start(void) __banked
{ {
if (pop_snd_ok) return 0; if (pop_snd_ok) return 0;
if (!snd_blk) return -1; /* набор не загружен */ if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR, if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, pop_sfx_fill) != 0) CBL_UNDERRUN_APP, pop_sfx_fill) != 0)
return -1; return -1;
@@ -74,3 +76,22 @@ void pop_sfx_close(void) __banked
pop_sfx_pause(); pop_sfx_pause();
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; } if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
} }
/* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP
* (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл:
* иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это
* 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание
* при периоде 245 759). Открытие обратно дешёвое — cbl_open только
* настраивает железо, страницы набора никуда не девались. */
void pop_sfx_toggle(void) __banked
{
if (pop_snd_want) {
pop_sfx_pause(); /* pause сам не трогает want... */
pop_snd_want = 0; /* ...поэтому гасим его следом */
} else {
pop_snd_want = 1;
/* Набора может не быть вовсе (файлы не прочитались, страницы не
* дались) — тогда откатываем флаг, чтобы индикация не врала. */
if (pop_sfx_start() != 0) pop_snd_want = 0;
}
}
+25 -8
View File
@@ -318,7 +318,7 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
* Флаги «фронта» стали статиками модуля — раньше они были локалями main. * Флаги «фронта» стали статиками модуля — раньше они были локалями main.
* Смысл прежний: одно нажатие = одно срабатывание. * Смысл прежний: одно нажатие = одно срабатывание.
*/ */
static uint8_t cheat_prev, immo_prev, sword_prev, nlvl_prev; static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev; static uint8_t upsd_prev, xdec_prev, xinc_prev;
void pop_cheat_tick(void) __banked void pop_cheat_tick(void) __banked
@@ -328,8 +328,10 @@ void pop_cheat_tick(void) __banked
* K — убить стража; * K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет: * I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза); * иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч * Ctrl+S — вкл/выкл звук (порт Ctrl+S из SDLPoP, seg000:657).
* берётся с пола (pop_have_sword ставит подбор); * НЕ чит: работает и без pop_cheats, как в оригинале.
* Ctrl обязателен — голая S у нас свободна, но менять
* пользовательскую раскладку оригинала незачем;
* Shift+L — следующий уровень (seg000:698, ветка next_level = * Shift+L — следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ * current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift — отладочный * тестировать уровни 2..15. NB: L без Shift — отладочный
@@ -346,7 +348,8 @@ void pop_cheat_tick(void) __banked
* следующего кадра. */ * следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL); uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO); uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD); uint8_t s = (uint8_t)(kbd_raw_down(KBD_SOUND_TOGGLE) &&
(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL)));
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) && uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))); (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE); uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
@@ -357,7 +360,7 @@ void pop_cheat_tick(void) __banked
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */ * мелкий урон» -> 0. Что означают уровни — pop_state.h. */
if (pop_cheats && i && !immo_prev) if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1); pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (pop_cheats && s && !sword_prev) pop_have_sword = 1; if (s && !sound_prev) pop_sfx_toggle(); /* не под pop_cheats */
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL) if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1); pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале). /* U — переворот экрана (seg000:0793 — чит и в оригинале).
@@ -370,7 +373,7 @@ void pop_cheat_tick(void) __banked
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1); if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k; cheat_prev = k;
immo_prev = i; immo_prev = i;
sword_prev = s; sound_prev = s;
nlvl_prev = nl; nlvl_prev = nl;
upsd_prev = up; upsd_prev = up;
xdec_prev = xd; xdec_prev = xd;
@@ -388,6 +391,11 @@ void pop_cheat_tick(void) __banked
* белый x 100..175 комната (1..24) * белый x 100..175 комната (1..24)
* голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST) * голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST)
* красный x 220..230 бессмертие (1 палочка = только бой, 2 = и мелкий урон) * красный x 220..230 бессмертие (1 палочка = только бой, 2 = и мелкий урон)
* пурпурный x 245 звук ВЫКЛЮЧЕН (Ctrl+S) либо не поднялся вовсе
* Звук показан «от противного» — палочка есть, когда звука НЕТ. Иначе
* индикатор ничего не сообщает: включённый звук слышно и так, а вот
* молчание неотличимо от «в этот момент нечему звучать».
* *
* ЦВЕТА — индексы ИГРОВОЙ палитры, и выбраны они не по вкусу, а по двум * ЦВЕТА — индексы ИГРОВОЙ палитры, и выбраны они не по вкусу, а по двум
* ограничениям. * ограничениям.
@@ -413,6 +421,7 @@ static uint8_t room_label_sig = 0xFF; /* последняя показанна
static uint8_t lvl_label_sig = 0xFF; /* ...уровень */ static uint8_t lvl_label_sig = 0xFF; /* ...уровень */
static uint8_t spd_label_sig = 0xFF; /* ...режим скорости */ static uint8_t spd_label_sig = 0xFF; /* ...режим скорости */
static uint8_t imm_label_sig = 0xFF; /* ...бессмертие */ static uint8_t imm_label_sig = 0xFF; /* ...бессмертие */
static uint8_t snd_label_sig = 0xFF; /* ...звук (pop_snd_want) */
static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */ static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */
/* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */ /* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */
@@ -428,17 +437,21 @@ void pop_dbg_room_label(uint8_t room) __banked
* (`if (n != g) g = n;` пишет разность вместо значения). */ * (`if (n != g) g = n;` пишет разность вместо значения). */
uint8_t prev_room = room_label_sig, prev_lvl = lvl_label_sig; uint8_t prev_room = room_label_sig, prev_lvl = lvl_label_sig;
uint8_t prev_spd = spd_label_sig, prev_imm = imm_label_sig; uint8_t prev_spd = spd_label_sig, prev_imm = imm_label_sig;
uint8_t prev_snd = snd_label_sig;
uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal; uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal;
uint8_t snd = pop_snd_want;
room_label_sig = room; room_label_sig = room;
lvl_label_sig = lvl; lvl_label_sig = lvl;
spd_label_sig = spd; spd_label_sig = spd;
imm_label_sig = imm; imm_label_sig = imm;
if (room != prev_room || lvl != prev_lvl || spd != prev_spd || imm != prev_imm) snd_label_sig = snd;
if (room != prev_room || lvl != prev_lvl || spd != prev_spd ||
imm != prev_imm || snd != prev_snd)
room_label_redraw = 2; room_label_redraw = 2;
if (!room_label_redraw) return; if (!room_label_redraw) return;
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
bar(10, 0, 230, 5); bar(10, 0, 250, 5);
setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */ setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */
label_bars(10, (uint8_t)(lvl / 10)); label_bars(10, (uint8_t)(lvl / 10));
@@ -457,6 +470,10 @@ void pop_dbg_room_label(uint8_t room) __banked
setfillstyle(SOLID_FILL, 0x3C); setfillstyle(SOLID_FILL, 0x3C);
label_bars(220, imm); /* палочек = уровень (1 или 2) */ label_bars(220, imm); /* палочек = уровень (1 или 2) */
} }
if (!snd) { /* звука НЕТ — пурпурный */
setfillstyle(SOLID_FILL, 0x3D);
label_bars(245, 1);
}
room_label_redraw--; room_label_redraw--;
} }