From e0c86a96ef68da98b6f36c77a9b695a2b45f526c Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 20 Aug 2026 15:49:33 +0300 Subject: [PATCH] =?UTF-8?q?Ctrl+S=20=E2=80=94=20=D0=B2=D0=BA=D0=BB/=D0=B2?= =?UTF-8?q?=D1=8B=D0=BA=D0=BB=20=D0=B7=D0=B2=D1=83=D0=BA;=20=D1=87=D0=B8?= =?UTF-8?q?=D1=82=20=C2=AB=D0=B2=D1=8B=D0=B4=D0=B0=D1=82=D1=8C=20=D0=BC?= =?UTF-8?q?=D0=B5=D1=87=C2=BB=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Порт 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 --- applications/PoP/docs/sound_plan.md | 70 +++++++++++++++++++++++ applications/PoP/roomtest/CLAUDE.md | 5 +- applications/PoP/roomtest/pop_cheat.h | 4 +- applications/PoP/roomtest/pop_sfx.c | 12 ++++ applications/PoP/roomtest/pop_sfx.h | 11 ++++ applications/PoP/roomtest/pop_sfx_cold.c | 25 +++++++- applications/PoP/roomtest/roomtest_cold.c | 33 ++++++++--- 7 files changed, 147 insertions(+), 13 deletions(-) diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index d3369fb..62d8248 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -383,3 +383,73 @@ IBM PC 1989 года: один квадратный голос. Он у нас DOS-версией; у нас последовательности разведены по доменам (`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 вплотную. diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index 395da68..ceae1d2 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -60,8 +60,9 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`). дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр); **1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I** — -бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень, -**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру +бессмертие, **Shift+L** — следующий уровень, +**+/−** — обход комнат (`ROOMNAV`). Не чит, а штатная клавиша оригинала: +**Ctrl+S** — вкл/выкл звук (пурпурная палочка в борте = звука нет). Уровень грузится по номеру (`pop_level_load_num`), на диске лежат все 15. ## Модули diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index a489a98..d69821b 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -22,7 +22,9 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ #define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида по кругу * 0 -> 1 (только бой) -> 2 (и мелкий урон) -> 0; * уровни расписаны в 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_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */ #define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */ diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index a5a30c9..7de2b8e 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -28,6 +28,18 @@ uint8_t pop_snd_page[POP_SND_PAGES]; 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: пишет главный цикл, читает прерывание. * Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory * avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на diff --git a/applications/PoP/roomtest/pop_sfx.h b/applications/PoP/roomtest/pop_sfx.h index cdc155f..f285b06 100644 --- a/applications/PoP/roomtest/pop_sfx.h +++ b/applications/PoP/roomtest/pop_sfx.h @@ -38,6 +38,17 @@ void pop_sfx_pause(void) __banked; /* Закрыть CBL и отпустить страницы; звать на выходе. */ 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 — нумерация оригинала, см. * sound_plan.md). Перебивает играющий. У звуков без оцифровки (это * музыка) длина 0 — вызов просто глушит текущий. diff --git a/applications/PoP/roomtest/pop_sfx_cold.c b/applications/PoP/roomtest/pop_sfx_cold.c index 316fc81..cdecf55 100644 --- a/applications/PoP/roomtest/pop_sfx_cold.c +++ b/applications/PoP/roomtest/pop_sfx_cold.c @@ -32,6 +32,7 @@ int pop_sfx_init(void) __banked } } snd_blk = blk; + pop_snd_want = 1; /* набор есть — звук по умолчанию включён */ return 0; } @@ -52,8 +53,9 @@ int pop_sfx_init(void) __banked * резиденте W2 тесная, и её отказ ронял бы весь звук. */ int pop_sfx_start(void) __banked { - if (pop_snd_ok) return 0; - if (!snd_blk) return -1; /* набор не загружен */ + if (pop_snd_ok) return 0; + 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, CBL_UNDERRUN_APP, pop_sfx_fill) != 0) return -1; @@ -74,3 +76,22 @@ void pop_sfx_close(void) __banked pop_sfx_pause(); 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; + } +} diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 92c5e46..cf404ad 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -318,7 +318,7 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked * Флаги «фронта» стали статиками модуля — раньше они были локалями 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; void pop_cheat_tick(void) __banked @@ -328,8 +328,10 @@ void pop_cheat_tick(void) __banked * K — убить стража; * I — бессмертие Кида (наш отладочный, в оригинале его нет: * иначе боёвку проверять получается ровно три раза); - * S — выдать меч, не бегая за ним в комнату 15. Штатно меч - * берётся с пола (pop_have_sword ставит подбор); + * Ctrl+S — вкл/выкл звук (порт Ctrl+S из SDLPoP, seg000:657). + * НЕ чит: работает и без pop_cheats, как в оригинале. + * Ctrl обязателен — голая S у нас свободна, но менять + * пользовательскую раскладку оригинала незачем; * Shift+L — следующий уровень (seg000:698, ветка next_level = * current_level + 1). Единственный вменяемый способ * тестировать уровни 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 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) && (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))); uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE); @@ -357,7 +360,7 @@ void pop_cheat_tick(void) __banked * мелкий урон» -> 0. Что означают уровни — pop_state.h. */ if (pop_cheats && i && !immo_prev) 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) pop_next_level = (uint8_t)(pop_current_level + 1); /* 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); cheat_prev = k; immo_prev = i; - sword_prev = s; + sound_prev = s; nlvl_prev = nl; upsd_prev = up; xdec_prev = xd; @@ -388,6 +391,11 @@ void pop_cheat_tick(void) __banked * белый x 100..175 комната (1..24) * голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST) * красный 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 spd_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 = обе страницы) */ /* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */ @@ -428,17 +437,21 @@ void pop_dbg_room_label(uint8_t room) __banked * (`if (n != g) g = n;` пишет разность вместо значения). */ 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_snd = snd_label_sig; uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal; + uint8_t snd = pop_snd_want; room_label_sig = room; lvl_label_sig = lvl; spd_label_sig = spd; 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; if (!room_label_redraw) return; setfillstyle(SOLID_FILL, BLACK); - bar(10, 0, 230, 5); + bar(10, 0, 250, 5); setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */ label_bars(10, (uint8_t)(lvl / 10)); @@ -457,6 +470,10 @@ void pop_dbg_room_label(uint8_t room) __banked setfillstyle(SOLID_FILL, 0x3C); label_bars(220, imm); /* палочек = уровень (1 или 2) */ } + if (!snd) { /* звука НЕТ — пурпурный */ + setfillstyle(SOLID_FILL, 0x3D); + label_bars(245, 1); + } room_label_redraw--; }