From 16d3262340e318f39b39824aae8bbe5433196a2a Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 25 Aug 2026 19:36:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B2=D1=83=D0=BA:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D0=BE=D1=81=20CBL=20=D0=BA=D0=B0=D1=87=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20W3=20=E2=80=94=20=D0=B2=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=B2=20BIOS=20=D0=BB=D0=BE=D0=BC=D0=B0=D0=BB?= =?UTF-8?q?=20W0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit СИМПТОМ: затемнение (fade) хрипело — одинаково с играющей музыкой и в тишине. Ключевое наблюдение пользователя: повтор тишины обязан звучать тишиной, значит дело не в недоливе буфера. ПОИСК: отладочные клавиши, каждая делала ровно один кусок fade. Ожидание кадров — чисто; чтение палитры, запись палитры и 512 вызовов bios_get_place() (видео вообще не трогает) — скрежет во всех трёх. Последнее и решило: виновата не палитра, а ЛЮБОЙ вызов BIOS. ПРИЧИНА: `rst 8` раскрывается в `out ($7C),a`, который включает системное ПЗУ и перестраивает окно 0 (MAME sprinter.cpp, update_memory: m_pages[0] + m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому запись в порт 0x82 из прерывания бесполезна — OTIR вычитывает ПЗУ и отдаёт его в звук. Отсюда же старое правило «глушить CBL на время загрузки файлов»: причина была не в том, что ESTEX долго занимает CPU. РЕШЕНИЕ (идея пользователя): качать через W3. Он управляется только портом 0xE2, подмену из прерывания никто не перекрывает, а BIOS во время нашего ISR не исполняется — окно возвращается до выхода, и для него подмена невидима. После этого BIOS безопасен везде. * pop_sfx.c — насос берёт взаймы W3 вместо W0, чтение по 0xC000 + смещение. * pop_ui.c — буфер палитры по фиксированному 0x4000 (эти 256 байт DSS занимает только при загрузке программы): 256 байт со стека долой. * libbgi/common/gfx_pal_write.c — запись палитры прямо в видеопамять, минуя BIOS. Писалась как обход скрежета, после переноса насоса не нужна; оставлена как более быстрый примитив (2,5 тыс. тактов на 64 цвета против 10,8 тыс. у BIOS) с честной шапкой. Адресация разобрана по исходникам BIOS (FUNC_SCREEN.ASM): Port_Y = индекс цвета, адрес 0xC3E0 + pal*4, порядок R/G/B/Y. * libc/video/pal_get.c, pal_load.c — в шапках зафиксировано, что BIOS выбирает окно ПО АДРЕСУ БУФЕРА (`BIT 7,H`). * pop_intro.c — при пропуске интро клавишей не глушился CBL, и следующая загрузка уровня шла с открытым буфером; добавлен pop_sfx_pause. * pop_ctrl.c — убраны отладочные «осторожные шаги» на J/L (эмуляция Shift+стрелка для MAME), у них и стоял TODO. Разбор целиком — docs/sound_plan.md §5. Бюджет: _CODE 23966, куча 245 Б. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/sound_plan.md | 57 +++++++++++++++++++++++++++ applications/PoP/roomtest/pop_cheat.h | 1 + applications/PoP/roomtest/pop_ctrl.c | 13 +----- applications/PoP/roomtest/pop_intro.c | 18 +++++++++ applications/PoP/roomtest/pop_sfx.c | 32 +++++++++++---- applications/PoP/roomtest/pop_ui.c | 21 +++++++--- libbgi/common/gfx_pal_write.c | 51 ++++++++++++++++++++++++ libbgi/include/gfx.h | 6 +++ libc/video/pal_get.c | 8 ++++ libc/video/pal_load.c | 8 ++++ 10 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 libbgi/common/gfx_pal_write.c diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index bbe3232..fcdaf6c 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -734,3 +734,60 @@ DOS-версии** — `applications/PoP/PoP1_DOS_music` (flac/mp3/ogg/ogg_MT-32 переносить, либо заводить банк 12. * `won` (77 страниц) в POP_MUS_PAGES=20 не помещается: финал придётся либо резать, либо стримить кусками по ходу. + + +## 5. СКРЕЖЕТ ПРИ BIOS-ВЫЗОВАХ: причина и решение (2026-08-25) + +Симптом: во время затемнения (fade) звук хрипел — одинаково с играющей +музыкой и в тишине. Пользователь заметил ключевое: **повтор тишины обязан +звучать тишиной**, значит дело не в недоливе буфера. + +### Как искали + +Отладочные клавиши, каждая делает ровно один кусок fade: + +| клавиша | что делала | результат | +|---|---|---| +| H | только ожидание 8 кадров | чисто | +| V | только чтение палитры | скрежет | +| G | затемнение целиком | скрежет | +| J | только запись палитры | скрежет | +| L | 512 раз `bios_get_place()` — видео не трогает | **скрежет** | + +`L` и решил вопрос: виновата не палитра, а **любой вызов BIOS**. + +### Причина + +Вход в BIOS — это `rst 8`, то есть `out ($7C),a`. В драйвере MAME он +правит `m_rom_sys` и вызывает `update_memory()`, которая перестраивает +**окно 0**: `m_pages[0]` + `m_bank_view0.select(1)`. ПЗУ ложится ПОВЕРХ +страничного регистра. + +Насос CBL брал окно взаймы именно у W0 (`_io_page_w0 = phys` + `OTIR` по +адресу < 0x4000). Пока BIOS работает, запись в порт `0x82` ничего не +меняет, и `OTIR` вычитывает ПЗУ, отдавая его в звук. + +Побочно выяснилось, почему `DI` вокруг BIOS помогал лишь иногда: он не +даёт войти в ISR (тогда блок просто пропускается, что неслышно), но в +обработчиках BIOS есть `EI`, так что защита негарантированная. + +### Решение + +Насос переведён на **W3** (идея пользователя): это окно управляется только +портом `0xE2`, подмену из прерывания никто не перекрывает, а BIOS во время +нашего ISR не исполняется — окно возвращается до выхода. + +```c +saved = _io_page_w3; +_io_page_w3 = phys; +cbl_push_otir((const void *)(0xC000u + ptr), n); +_io_page_w3 = saved; +``` + +После этого BIOS безопасен везде: и палитра, и любые другие функции. +Временный обход палитры мимо BIOS (`gfx_pal_write`) стал не нужен — он +остался в libbgi как более быстрый примитив (2,5 тыс. тактов на 64 цвета +против 10,8 тыс. у BIOS), но игра его не зовёт. + +Бонус: в W3 нет стаба восстановления окна, который в W0 занимал начало +страницы, — звуковые страницы можно использовать целиком. diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index c9fd28b..345ce68 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -39,6 +39,7 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ #define KBD_CHEAT_TIME_SUB 0x83 /* F7 (PS/2 set 2) — отнять минуту */ #define KBD_CHEAT_TIME_ADD 0x0A /* F8 (PS/2 set 2) — добавить минуту */ + /* Старое имя оставлено для обработчика Shift+L; граница одна — pop_level.h. */ #define POP_LAST_LEVEL POP_LEVEL_LAST diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 73ef3da..1d27649 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -24,12 +24,6 @@ #include "pop_map.h" #include "pop_guard.h" /* charid, состояние меча, seq стража */ -/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в - * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 - * scancodes. TODO: убрать после отладки. */ -#define KBD_DBG_STEPL 0x3B /* J */ -#define KBD_DBG_STEPR 0x4B /* L */ - /* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */ #define CONTROL_RELEASED 0 #define CONTROL_IGNORE 1 @@ -632,14 +626,9 @@ static void read_input(void) uint8_t r = kbd_raw_down(KBD_RIGHT); uint8_t u = kbd_raw_down(KBD_UP); uint8_t d = kbd_raw_down(KBD_DOWN); - /* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */ - uint8_t sl = kbd_raw_down(KBD_DBG_STEPL); - uint8_t sr = kbd_raw_down(KBD_DBG_STEPR); - if (sl) l = 1; - if (sr) r = 1; control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED); control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED); - control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) + control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) ? CONTROL_HELD : CONTROL_RELEASED; } diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c index 668a152..7b9b1a1 100644 --- a/applications/PoP/roomtest/pop_intro.c +++ b/applications/PoP/roomtest/pop_intro.c @@ -1315,10 +1315,28 @@ uint8_t pop_intro_show(void) __banked pop_music_play(); } if (intro_run(intro_before_pv)) { + /* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт + * загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше, + * чем играет буфер, и железо начинает крутить свои 256 байт по + * кругу — на слух ровно тот скрежет, что ловится нажатием клавиши + * на первом экране истории. В обычной ветке ниже pause уже есть, + * а здесь его не было. */ pop_music_free(); + pop_sfx_pause(); intro_restore_game_palette(); return 1; } + /* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на + * переходе к PV: fade уводит картинку, а музыка продолжается, и лишь + * когда она смолкла, начинается сцена с принцессой (у SDLPoP это + * общий приём — `while (check_sound_playing())`, seg000:2027). + * Мы освобождали страницы сразу после fade, и трек обрывался на + * полуслове — на слух это и был скрежет в конце затемнения. */ + { + intro_skip_t tail; + intro_skip_begin(&tail); + while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge(); + } pop_music_free(); pop_sfx_pause(); pv_result = intro_pv_animated(); diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index 8788ec0..a0162af 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -59,10 +59,25 @@ static volatile uint16_t sfx_left; /* сколько байт осталось /* ---- насос ---------------------------------------------------------- * * Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами. * - * ОКНО W0 БЕРЁМ ВЗАЙМЫ. Фон в этот момент может держать в W0 страницу - * атласа (pop_blit_b делает map ... blit ... unmap), поэтому сохраняем - * порт, подставляем свою страницу и возвращаем как было. Для фона - * подмена невидима: прерывание для него атомарно. Тот же приём, что у + * ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — и это принципиально. + * + * Раньше насос занимал W0. Оказалось, что так звук рвёт ЛЮБОЙ вызов + * BIOS: `rst 8` — это `out ($7C),a`, который включает системное ПЗУ и + * перестраивает окно 0 (в MAME это update_memory: m_pages[0] + + * m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому + * запись в порт 0x82 из прерывания ничего не даёт — OTIR честно + * вычитывает ПЗУ и отдаёт его в CBL. На слух это скрежет; поймано на + * затемнении в PoP (2026-08-25), причём одинаково при чтении палитры, + * записи палитры и вообще любой функции BIOS. + * + * W3 таким свойством не обладает: он управляется ТОЛЬКО портом 0xE2, и + * подмену из прерывания никто не перекрывает. BIOS тоже двигает W3 + * (LP_OPEN_PG, PIC_SET_PAL), но во время нашего ISR он не исполняется — + * мы возвращаем окно до выхода, и для него подмена невидима. Ровно так + * же незаметна она и для фона с accel-блитом: их DI-банды прерывание не + * рвёт, а между бандами W3 всегда восстановлен. + * + * Идея пользователя, 2026-08-25. Тот же приём заимствования окна, что у * трамплина с W1 (_irq_tramp.c). * * Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от @@ -91,10 +106,11 @@ int pop_sfx_fill(uint16_t n) phys = pop_snd_page[POP_SND_SILENCE_PAGE]; ptr = POP_SND_SILENCE_OFF; } - saved = _io_page_w0; - _io_page_w0 = phys; - cbl_push_otir((const void *)ptr, n); - _io_page_w0 = saved; + /* ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — см. шапку файла. */ + saved = _io_page_w3; + _io_page_w3 = phys; + cbl_push_otir((const void *)(0xC000u + ptr), n); + _io_page_w3 = saved; if (!sfx_left) { if (!pop_mus_left) return 1; /* тишина — курсор не двигаем */ /* Длину трека считаем ПОРЦИЯМИ насоса, а не байтами: 169 КБ в diff --git a/applications/PoP/roomtest/pop_ui.c b/applications/PoP/roomtest/pop_ui.c index 758ee6d..6b08bf8 100644 --- a/applications/PoP/roomtest/pop_ui.c +++ b/applications/PoP/roomtest/pop_ui.c @@ -28,6 +28,12 @@ static const pop_font_desc_t font_desc[] = { static atlas_t font_at; static uint8_t font_ready; +/* БУФЕР ПАЛИТРЫ ПО ФИКСИРОВАННОМУ АДРЕСУ 0x4000. DSS занимает эти 256 + * байт только при загрузке программы, дальше они наши (код начинается с + * 0x4100), и держать буфер здесь дешевле, чем класть 256 байт на стек в + * каждом вызове — идея пользователя, 2026-08-25. */ +__at (0x4000) uint8_t pop_pal_buf[256]; + static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch) { if (ch < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?'; @@ -127,14 +133,18 @@ static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store) gfx_w0_unmap(); } +/* Снимок палитры целиком: дробить на мелкие порции больше не нужно — + * причина скрежета была не в длительности BIOS-вызова, а в том, какое окно + * он под себя забирал (см. pop_pal_buf выше). */ + void pop_ui_palette_snapshot(void) __banked { - uint8_t chunk, buf[256]; + uint8_t chunk; if (!font_ready) return; for (chunk = 0; chunk < 4; chunk++) { - gfx_pal_get(0, (uint8_t)(chunk << 6), 64, buf); - snapshot_chunk(chunk, buf, 1); + gfx_pal_get(0, (uint8_t)(chunk << 6), 64, pop_pal_buf); + snapshot_chunk(chunk, pop_pal_buf, 1); } } @@ -143,7 +153,8 @@ void pop_ui_palette_snapshot(void) __banked * в постоянно вызываемый (во время fade) путь. */ void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked { - uint8_t chunk, i, start, buf[256]; + uint8_t chunk, i, start; + uint8_t *buf = pop_pal_buf; if (!font_ready) return; if (dim_step > 4) dim_step = 4; @@ -184,7 +195,7 @@ void pop_ui_palette_black(void) __banked { uint8_t chunk; uint16_t i; - uint8_t black[256]; + uint8_t *black = pop_pal_buf; /* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем * все четыре четверти палитры, не занимая постоянную память. */ diff --git a/libbgi/common/gfx_pal_write.c b/libbgi/common/gfx_pal_write.c new file mode 100644 index 0000000..f4e2293 --- /dev/null +++ b/libbgi/common/gfx_pal_write.c @@ -0,0 +1,51 @@ +/* + * gfx_pal_write — записать блок палитровых записей ПРЯМО в видеопамять, + * минуя BIOS $A4. + * + * КОГДА НУЖНА. Штатный gfx_pal_load идёт через BIOS, а вход в BIOS + * (`rst 8` = `out ($7C),a`) включает системное ПЗУ и перестраивает окно 0. + * Пока это так, любой код, читающий данные ИЗ W0 в прерывании, получит + * вместо них ПЗУ. В PoP на этом рвался звук, пока насос CBL брал взаймы + * W0; после переноса насоса на W3 (см. pop_sfx.c) BIOS снова безопасен, и + * в самой игре этот примитив не нужен. + * + * Остаётся полезен там, где BIOS звать нельзя по другим причинам, и просто + * как более быстрый путь: цикл ниже кладёт 64 цвета примерно за 2,5 тыс. + * тактов против 10,8 тыс. у BIOS (замеры в MAME, 2026-08-25). + * + * КАК АДРЕСУЕТСЯ ПАЛИТРА (исходник BIOS, FUNC_SCREEN.ASM, PIC_SET_PAL). + * Port_Y выбирает ИНДЕКС ЦВЕТА и инкрементируется на каждый цвет; адрес + * записи — 0xC3E0 + pal*4, внутри четвёрки порядок R, G, B, Y, а на входе + * данные лежат как B, G, R, Y. Окно на время работы держит страницу 0x50. + */ +#include "../_bgi.h" +#include + +/* Port_Y (0x89) как SFR: у libc готового объявления нет, а держать здесь + * asm-вставку ради одного OUT дороже, чем одна строка. */ +__sfr __at PORT_RGADR _io_port_y; + +/* Палитровые байты начинаются в килобайтной строке со смещения 0x3E0. */ +#define PAL_SLOT0 0xC3E0u + +void gfx_pal_write(uint8_t pal_num, uint8_t start, uint8_t count, + const uint8_t *bgr0) +{ + uint8_t *slot = (uint8_t *)(PAL_SLOT0 + ((uint16_t)pal_num << 2)); + uint8_t saved_bank = _gfx_bank; + uint8_t y = start; + + /* Писать обязательно ОБЫЧНЫМ банком: в прозрачном (0x5C) байт 0xFF не + * записывается, и белые компоненты палитры молча терялись бы. */ + _gfx_bank = GFX_BANK_NORMAL; + _bgi_begin(); + do { /* count == 0 означает 256 цветов */ + _io_port_y = y++; + slot[2] = *bgr0++; /* B */ + slot[1] = *bgr0++; /* G */ + slot[0] = *bgr0++; /* R */ + slot[3] = *bgr0++; /* Y */ + } while (--count); + _bgi_end(); + _gfx_bank = saved_bank; +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 5989240..ab75f48 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -274,6 +274,12 @@ void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count, const uint8_t *data); void gfx_pal_set (uint8_t pal_num, uint8_t idx, uint8_t r, uint8_t g, uint8_t b); +/* Записать палитру ПРЯМО в видеопамять, минуя BIOS $A4. Нужна там, где + * рядом играет звук: штатный путь через BIOS давал скрежет в CBL (разбор — + * в шапке common/gfx_pal_write.c). Формат тот же, что у gfx_pal_load. */ +void gfx_pal_write(uint8_t pal_num, uint8_t start, uint8_t count, + const uint8_t *bgr0); + void gfx_pal_get (uint8_t pal_num, uint8_t start, uint8_t count, uint8_t *data); void gfx_pal_get_color(uint8_t pal_num, uint8_t idx, diff --git a/libc/video/pal_get.c b/libc/video/pal_get.c index 8cda7a1..81eac5c 100644 --- a/libc/video/pal_get.c +++ b/libc/video/pal_get.c @@ -1,6 +1,14 @@ /* * pal_get — прочитать блок палитровых записей из VRAM (BIOS $A4; * тот же id функции, бит 7 регистра A переключает в чтение). + * + * ВНИМАНИЕ: BIOS выбирает окно ПО АДРЕСУ БУФЕРА (исходник + * docs/sources/Sprinter-BIOS/bios/exp/FUNC_SCREEN.ASM, PIC_SET_PAL: + * `BIT 7,H` -> SLOT3 либо SLOT1). Буфер выше 0x8000 заставляет его занять + * W1, где у приложений лежат резидентный код и вектор прерываний, — и всё + * время цикла ISR недоступен. Если рядом играет звук, пользуйтесь + * gfx_pal_read/gfx_pal_write из libbgi: они пишут видеопамять напрямую и + * BIOS не зовут. */ #include "_palette.h" diff --git a/libc/video/pal_load.c b/libc/video/pal_load.c index ed0db36..25abe0f 100644 --- a/libc/video/pal_load.c +++ b/libc/video/pal_load.c @@ -2,6 +2,14 @@ * pal_load — записать блок палитровых записей в VRAM * (BIOS $A4 PIC_SET_PAL): pal_num 0..7, start — первый слот, * count слотов (0 = 256), bgr0 — записи по 4 байта (B, G, R, 0). + * + * ВНИМАНИЕ: BIOS выбирает окно ПО АДРЕСУ БУФЕРА (исходник + * docs/sources/Sprinter-BIOS/bios/exp/FUNC_SCREEN.ASM: `BIT 7,H` -> SLOT3 + * либо SLOT1). Буфер выше 0x8000 — а на стеке он всегда там — заставляет + * BIOS занять W1, где лежат резидентный код и вектор прерываний; на это + * время ISR недоступен. В PoP это слышно как скрежет при затемнении. + * Рядом со звуком пользуйтесь gfx_pal_write из libbgi: он пишет + * видеопамять напрямую. */ #include "_palette.h"