From a630568a8b6238b3c0ffb7ae1f34fa845d939cf0 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 20 Aug 2026 16:46:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B2=D1=83=D0=BA=20PoP:=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BE=D1=80=D0=B8=D1=82=D0=B5=D1=82=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20=C2=AB=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B1=D0=B8=D0=B2=D0=B0=D1=82=D1=8C=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась приземлением Кида, в оригинале доигрывает до конца, а приземления не слышно. Оказалось, упущен целый механизм. play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на кадр — из нескольких выживает важнейший (меньше prio = важнее, при равенстве последний). play_next_sound (seg000:1304) раз в кадр решает, запускать ли: можно, только если ничего не играет ЛИБО текущий помечен перебиваемым и новый не менее важен. Иначе номинант выбрасывается — очереди в оригинале нет. Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому решётка звучит только в паузах между укусами. Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он определён безусловно). Створка двери уровня — единственная запись, правимая на ходу, вынесена в отдельный байт. Добавлен пропущенный stop_sounds на завершении открытия двери (seg007:455). Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со звуком 6. Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно только на железе. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/sound_plan.md | 65 ++++++++++++++++++ applications/PoP/roomtest/BUGS_OPEN.md | 27 ++++++++ applications/PoP/roomtest/pop_sfx.c | 91 +++++++++++++++++++++++++- applications/PoP/roomtest/pop_sfx.h | 19 +++++- applications/PoP/roomtest/pop_trob.c | 10 ++- applications/PoP/roomtest/roomtest.c | 7 ++ 6 files changed, 211 insertions(+), 8 deletions(-) diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index e24717d..27c92d9 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -514,3 +514,68 @@ ESTEX на каждый атлас) ПРИ ОТКРЫТОМ звуке, насо Лечение: `pop_sfx_close()` первым действием `pop_shutdown`. Проверено второй записью — всплеска на выходе больше нет. Механизм тот же, из-за которого звук глушится на время загрузки уровня. + +## 11. Приоритеты и перебиваемость: звук у оригинала НЕ «всегда перебивать» + +Пользователь услышал расхождение: у нас решётка обрывалась приземлением +Кида, в SDLPoP — доигрывала до звонкого конца, а приземления не было +слышно вовсе. Разбор исходника показал, что мы упустили ЦЕЛЫЙ МЕХАНИЗМ. + +### 11.1 Модель оригинала + +``` +play_sound(id) seg000:12C5 — только НОМИНИРУЕТ кандидата на кадр: + if next < 0 || prio[id] <= prio[next]: next = id + +play_next_sound() seg000:1304 — раз в кадр решает, запускать ли: + if next >= 0: + if !играет_что_то || + (перебиваем[текущий] && prio[next] <= prio[текущий]): + текущий = next; запустить + next = -1 // НЕ запустили -> номинант ВЫБРОШЕН, очереди нет +``` + +Три следствия, каждое слышно: + +- **Неперебиваемый звук доигрывает целиком.** У `gate_closing_fast` (6) + `interruptible = 0`, поэтому приземление Кида (17) в этот момент + пропадает совсем — не откладывается, а именно теряется. +- **Внутри кадра выживает важнейший.** Меньше `prio` — важнее; при + равенстве побеждает ПОСЛЕДНИЙ (сравнение `<=`). +- **Два источника не «чередуются как получится».** Челюсти (47, prio + 0x10) всегда важнее решётки (4, prio 0x32): решётка не может перебить + укус, а укус решётку — может. Отсюда и картина на ур. 9 к. 9, где + решётка звучит только в паузах между укусами. + +### 11.2 Что сделано у нас + +`pop_sfx_play` теперь только номинирует; запуск — в `pop_sfx_tick`, +который зовётся раз в кадр в конце отрисовки (там же, где оригинал зовёт +`play_next_sound`, seg000:954). Таблицы `snd_prio` (57 байт) и битовая +карта `snd_intr` (8 байт) — в резиденте, значения из SDLPoP С УЧЁТОМ +`fix_sound_priorities()`: в `config.h` SDLPoP `FIX_SOUND_PRIORITIES` +определён безусловно, значит сравниваемся мы с исправленным вариантом +(звук 10 → 0x0D, 48 → 0x15, 49 перебиваем). + +Створка двери уровня (15) — единственная запись, которую оригинал правит +на ходу: перебиваема при закрытии, нет при открытии (seg007:442/464). +Держим отдельным байтом `pop_sfx_slide_intr`, чтобы таблица осталась в +ПЗУ. Там же добавлен пропущенный `stop_sounds()` на завершении открытия +двери (seg007:455) — без него неперебиваемый съезд (1,6 с) блокировал бы +очередь. + +Звуки без оцифровки (музыка, длина 0) не номинируются вовсе — порт +проверки `if (NULL == sound_pointers[id]) return;`. Раньше такой id +глушил живой эффект. + +### 11.3 Проверка + +Записью MAME, старт уровня 1: + +| | всплески | что это | +|---|---|---| +| до | 135 мс + 210 мс | решётка, обрезанная приземлением на 80 мс | +| после | **один, 455 мс** | решётка целиком, корреляция огибающей со звуком 6 **+0,889** | + +Цена: резидент +~250 Б (таблицы + логика), куча ужалась с 347 до 134 Б — +довод в пользу давно назревшей реорганизации базовой памяти. diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index 1369e5b..07f0daa 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -893,3 +893,30 @@ Makefile правила нет. **Приоритет низкий:** ситуация возникает только когда труп остаётся на проваливающемся полу, играбельности не мешает. + +--- + +## BUG-SND-FIRSTRUN — при ПЕРВОМ запуске первый эффект слегка искажён + +**Симптом** (пользователь, 2026-08-20): после загрузки системы, при первом +запуске программы, самый первый звуковой эффект идёт с искажением; при +повторном запуске программы искажения нет. Наблюдалось ещё на тестовых +примерах CBL, до PoP. + +**Причина, скорее всего, уже устранена.** Аппаратный буфер CBL (256 +слотов) железо не чистит, а запись в порт управления сразу пускает +воспроизведение с нулевого слота — то есть первые 23,4 мс играет то, что +лежало в буфере раньше. При первом запуске это остатки после загрузки +системы, при втором — наша же тишина, поэтому и слышно только один раз. +Разбор — `../docs/sound_plan.md` §10.2. + +Лечение сделано в libc: `_cbl_prime` заливает буфер тишиной сразу после +включения (`cbl_open`). **В MAME проверить нельзя**: эмулируемый буфер +стартует нулями при двухдополнительном ЦАП, то есть там этот дефект нем +изначально — записи до и после фикса совпали побитово по огибающей. + +**Что нужно:** проверка на РЕАЛЬНОМ железе, первый запуск после холодной +загрузки. Если искажение осталось — значит мусор приходит не из буфера +CBL, и копать надо в системных буферах DSS. + +**Приоритет низкий:** один раз за сеанс, на играбельность не влияет. diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index 7de2b8e..a8ddfd0 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -87,13 +87,98 @@ int pop_sfx_fill(uint16_t n) return 1; } +/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ * + * + * Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель + * ровно такая: + * + * play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких + * за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее, + * при равенстве побеждает последний — сравнение <=); + * play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего + * не играет, ЛИБО текущий звук помечен «перебиваемым» + * И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ + * (next_sound = -1 безусловно), а не встаёт в очередь. + * + * Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и + * доигрывает до звонкого конца, а приземление Кида в этот момент пропадает + * совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и + * решётка звучит только в паузах между укусами. + * + * Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible + * data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP + * FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя + * именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем). + */ + +/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они + * никогда не номинируются (нет оцифровки), но индексация должна совпадать + * с оригиналом, иначе таблицу не сверить глазами. */ +static const uint8_t snd_prio[POP_SND_COUNT] = { + 0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */ + 0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */ + 0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */ + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */ + 0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */ + 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */ +}; + +/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3): + * 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня + * байт на счету, а читаем мы их раз в кадр. */ +static const uint8_t snd_intr[8] = { + /* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7), + /* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7), + /*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7), + /*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7), + /*32..39 */ 0, + /*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7), + /*48..55 */ 0, + /*56.. */ 0 +}; + +/* Створка двери уровня (15) — единственная запись, которую оригинал правит + * НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии + * нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим + * отдельным байтом, чтобы таблица осталась в ПЗУ. */ +uint8_t pop_sfx_slide_intr; + +/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано + * ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1; + * со смещением на единицу начальное состояние получается даром. */ +static uint8_t snd_next1; +static uint8_t snd_curr; /* что играет — для prio/intr */ + +static uint8_t snd_interruptible(uint8_t id) +{ + if (id == 15) return pop_sfx_slide_intr; + return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7))); +} + /* ---- запуск / останов ------------------------------------------------ */ void pop_sfx_play(uint8_t id) { - uint8_t pg; - uint16_t off, len; if (!pop_snd_ok || id >= POP_SND_COUNT) return; + /* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки + * `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id + * выигрывал бы конкурс и глушил живой эффект. */ + if (pop_snd_tbl[id].len == 0) return; + if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1]) + snd_next1 = (uint8_t)(id + 1); +} + +void pop_sfx_tick(void) +{ + uint8_t id, pg; + uint16_t off, len; + if (!snd_next1) return; + id = (uint8_t)(snd_next1 - 1); + snd_next1 = 0; /* номинант живёт ровно кадр */ + if (sfx_left != 0 && /* что-то играет... */ + (!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr])) + return; /* ...и уступать не обязано */ + snd_curr = id; pg = pop_snd_tbl[id].page; off = pop_snd_tbl[id].off; len = pop_snd_tbl[id].len; @@ -103,7 +188,7 @@ void pop_sfx_play(uint8_t id) IRQ_DISABLE(); sfx_pg = pg; sfx_ptr = off; - sfx_left = len; /* len == 0 — просто глушим */ + sfx_left = len; IRQ_ENABLE(); } diff --git a/applications/PoP/roomtest/pop_sfx.h b/applications/PoP/roomtest/pop_sfx.h index f285b06..a6b521c 100644 --- a/applications/PoP/roomtest/pop_sfx.h +++ b/applications/PoP/roomtest/pop_sfx.h @@ -49,14 +49,27 @@ void pop_sfx_toggle(void) __banked; * честно means «звука не будет». */ extern uint8_t pop_snd_want; -/* Начать эффект с номером id (0..56 — нумерация оригинала, см. - * sound_plan.md). Перебивает играющий. У звуков без оцифровки (это - * музыка) длина 0 — вызов просто глушит текущий. +/* Заявить эффект с номером id (0..56 — нумерация оригинала, см. + * sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий: + * это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на + * текущий кадр. Из нескольких заявок за кадр выживает важнейшая, а решение + * «запускать или нет» принимает pop_sfx_tick. Звуки без оцифровки (музыка) + * игнорируются. * * НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в * pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */ void pop_sfx_play(uint8_t id); +/* Раз в кадр: запустить номинанта, если можно (порт play_next_sound, + * seg000:1304). Оригинал зовёт его в конце отрисовки кадра — зовём там же. + * Не запустили — номинант ВЫБРАСЫВАЕТСЯ, очереди у оригинала нет. */ +void pop_sfx_tick(void); + +/* Перебиваемость съезжающей створки двери уровня (звук 15) — единственная + * запись таблицы, которую оригинал правит на ходу (seg007:442/464): + * 1 при закрытии двери, 0 при открытии. */ +extern uint8_t pop_sfx_slide_intr; + /* Оборвать текущий эффект (порт stop_sounds). */ void pop_sfx_stop(void); diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 7db3767..d6adaf5 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -529,12 +529,18 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room) if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */ else { *m = (uint8_t)(*m - sp); - if (*type == 4) pop_sfx_play(15); /* seg007:443 */ + /* seg007:442: створку НА ЗАКРЫТИИ перебивать можно. */ + if (*type == 4) { pop_sfx_slide_intr = 1; pop_sfx_play(15); } } } else { - if (*m < 43) { (*m)++; pop_sfx_play(15); } /* seg007:465 */ + /* seg007:464: на ОТКРЫТИИ створка неперебиваема — иначе съезд + * обрывается посреди и «конца» не слышно. */ + if (*m < 43) { (*m)++; pop_sfx_slide_intr = 0; pop_sfx_play(15); } if (*m >= 43) { *m = 43; *type = -1; + /* seg007:455: дверь открылась — оригинал глушит всё. Без этого + * неперебиваемый съезд (1,6 с) продолжал бы блокировать очередь. */ + pop_sfx_stop(); /* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз * за открытие; иначе зеркало ставилось бы заново каждый кадр, * пока дверь стоит открытой. */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index a2f05bd..3ff499f 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -18,6 +18,7 @@ #include "pop_trob.h" #include "pop_redraw.h" #include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */ +#include "pop_sfx.h" /* pop_sfx_tick — решение о запуске звука за кадр */ #include "pop_guard.h" #include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */ #include "pop_cheat.h" @@ -704,6 +705,12 @@ int main(void) PROF(0); /* чёрный: ждём vsync */ + /* Звук: решить, запускать ли заявленный за кадр эффект (порт + * play_next_sound). Место то же, что у оригинала — конец отрисовки + * кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается + * важнейшая. Раньше по кадру нельзя — потеряются поздние заявки. */ + pop_sfx_tick(); + /* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */