From 54112347f51ebb3d75f66c5d8e6ffcda4889590a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Sep 2026 20:14:06 +0300 Subject: [PATCH] Add large-file (>1MB) OPL4 loading and fix track-end hang - One-shot cold loader (asm/opl4_loader.asm, overlaid into plugin page 5) uploads embedded PCM sample blocks into ZXM-MoonSound SRAM and compacts the VGM stream in place, letting files over the normal 64-page megabuffer budget play back through the existing pipeline. - instant_abort() was filling only one of the two ISR command buffers with the shutdown chain; the chain (SILENCE_OPL + WAVE_OFF/TL + CMD_ISR_DONE) doesn't fit in one buffer, so after the first CMD_END_BUF the ISR swapped into stale data and never reached CMD_ISR_DONE, hanging at the end of long tracks instead of advancing to the next one. Now fills both buffers while frozen and syncs update_buffer()'s own ready-tracking state. - Switch to 14MHz before the SRAM upload (previously ran at whatever speed WC's file I/O left active) and show "Loading..." for .vgz and files >= 1MB, since the busy-waited upload takes several seconds. - Confirmed on real hardware: long OPL4 tracks (previously stuck looping at the end) now finish and advance normally, ESC exits cleanly. --- DiskRef/WC/VGMPLAY.WMF | Bin 98816 -> 98816 bytes VGMPLAY.WMF | Bin 98816 -> 98816 bytes src_sdcc/asm/cold_return.s | 8 + src_sdcc/asm/opl4_loader.asm | 708 ++++++++++++++++++++++++++++++++ src_sdcc/build.bat | 10 + src_sdcc/inc/variant_cfg.h | 2 +- src_sdcc/inc/vgm.h | 1 + src_sdcc/lib/vgm.c | 356 +++++++++++----- src_sdcc/main.c | 306 +++++++++++--- src_sdcc/scripts/overlay_bin.js | 14 + 10 files changed, 1240 insertions(+), 165 deletions(-) create mode 100644 src_sdcc/asm/cold_return.s create mode 100644 src_sdcc/asm/opl4_loader.asm create mode 100644 src_sdcc/scripts/overlay_bin.js diff --git a/DiskRef/WC/VGMPLAY.WMF b/DiskRef/WC/VGMPLAY.WMF index 7a9d79beb929e28c8b495163a83d87d5edafcc57..4714075e56dcdf72ade58da7eb5d859f8680fc2d 100644 GIT binary patch delta 11813 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 MiB. + ld hl,(#C004) + ld bc,4 + add hl,bc + ld de,(#C006) + jr nc,.size_ok + inc de +.size_ok: + ld a,d + or a + jp nz,.compact_fail + ld a,e + cp #10 + jr c,.count_pages + jp nz,.compact_fail + ld a,h + or l + jp nz,.compact_fail +.count_pages: + ld a,e + add a,a + add a,a + ld b,a + ld a,h + rlca + rlca + and 3 + add a,b + ld b,a + ld a,h + and #3F + or l + jr z,.pages_ready + inc b +.pages_ready: + ld a,b + ld (result_pages),a + + ; Copy [block_end,1MiB) forward using Win3 -> Win0. + ld bc,PORT_W0 + in a,(c) + push af + ld bc,PORT_W3 + in a,(c) + push af + xor a + ld (dst_page),a + ld hl,(block_end_ptr) + ld de,(dst_ptr) + ld a,(block_end_page) + ld (copy_src_page),a + add a,#20 + ld bc,PORT_W3 + out (c),a + ld a,#20 + ld bc,PORT_W0 + out (c),a +.copy: + ld a,(hl) + ld (de),a + inc hl + inc de + ld a,d + cp #40 + jr nz,.src_wrap + ld de,0 + ld a,(dst_page) + inc a + ld (dst_page),a + add a,#20 + ld bc,PORT_W0 + out (c),a +.src_wrap: + ld a,h + or l + jr nz,.copy + ld a,(copy_src_page) + inc a + ld (copy_src_page),a + cp 64 + jr z,.copy_done + add a,#20 + ld bc,PORT_W3 + out (c),a + ld hl,#C000 + jr .copy +.copy_done: + pop af + ld bc,PORT_W3 + out (c),a + pop af + ld bc,PORT_W0 + out (c),a + ld a,(dst_page) + ld h,a + ld a,d + or #C0 + ld l,a + ld a,(result_pages) + ret +.compact_fail: + xor a + ret + +sub_header32: + push hl + ld a,(hl) + inc hl + or (hl) + inc hl + or (hl) + inc hl + or (hl) + pop hl + ret z + push hl + ld e,(hl) + inc hl + ld d,(hl) + ex de,hl + ld bc,(delta) + or a + sbc hl,bc + ex de,hl + ld (hl),d + dec hl + ld (hl),e + pop hl + inc hl + inc hl + ld e,(hl) + inc hl + ld d,(hl) + ex de,hl + ld bc,(delta+2) + sbc hl,bc + ex de,hl + ld (hl),d + dec hl + ld (hl),e + ret + +read_byte: + ld hl,(src_ptr) + ld a,(hl) + inc hl + ld (src_ptr),hl + push af + ld a,h + or l + jr nz,.no_wrap + call next_src_page +.no_wrap: + pop af + ret + +next_src_page: + ld a,(src_page) + inc a + ld (src_page),a + call map_vpl + ld hl,#C000 + ld (src_ptr),hl + ret + +len_is_zero: + ld hl,block_len + ld a,(hl) + inc hl + or (hl) + inc hl + or (hl) + inc hl + or (hl) + ret + +dec_len: + ld hl,block_len + ld a,(hl) + sub 1 + ld (hl),a + inc hl + ld a,(hl) + sbc a,0 + ld (hl),a + inc hl + ld a,(hl) + sbc a,0 + ld (hl),a + inc hl + ld a,(hl) + sbc a,0 + ld (hl),a + ret + +sub_len_bc: + ld hl,(block_len) + or a + sbc hl,bc + ld (block_len),hl + ret nc + ld hl,(block_len+2) + dec hl + ld (block_len+2),hl + ret + +chip_wait: + in a,(OPL_STAT) + rrca + jr c,chip_wait + ret +wave_reg: + push af + call chip_wait + pop af + out (OPL_WAVE_REG),a + ret +wave_dat: + push af + call chip_wait + pop af + out (OPL_WAVE_DAT),a + ret + +map_page0: + xor a +map_vpl: + add a,#20 + ld bc,PORT_W3 + out (c),a + ret + +sig_vgm: DB "Vgm " +src_page: DB 0 +src_ptr: DW #C000 +block_end_page: DB 0 +block_end_ptr: DW 0 +block_type: DB 0 +block_count: DB 0 +block_len: DD 0 +start_lo: DB 0 +start_mid: DB 0 +start_hi: DB 0 +hdr_pos: DB 0 +hdr_left: DB 0 +rom_flag: DB 0 +delta: DD 0 +dst_page: DB 0 +dst_ptr: DW 0 +copy_src_page: DB 0 +result_pages: DB 0 + + ASSERT $ <= #B800, "OPL4 cold loader exceeds reserved overlay" + +; #6020 maps page 5 and jumps to the loader but deliberately does not restore +; Win2. Restore the physical page-0 mapping ourselves. OUT is placed at +; #BFFD; its following instruction is fetched from the newly mapped page 0, +; where cold_return.s puts a single RET at #BFFF. E preserves the loader's A +; result while D carries the physical page number to OUT (C),D, so returned A +; and HL remain intact. + ORG #BFEF +page0_return: + ld e,a + ld a,(#6002) + sub 5 + ld (#6002),a + ld d,a + ld a,e + ld bc,PORT_W2 ; #12AF, Win2 + out (c),d + ASSERT $ == #BFFF, "page0 return trampoline must end at #BFFE" + + SAVEBIN "build/opl4_loader.bin", #9800, $-#9800 diff --git a/src_sdcc/build.bat b/src_sdcc/build.bat index f028d01..489de76 100644 --- a/src_sdcc/build.bat +++ b/src_sdcc/build.bat @@ -52,9 +52,15 @@ REM Паддинг inflate.bin до 16KB (чтобы cmdblocks.bin начина powershell -NoProfile -Command "$f=[IO.File]::ReadAllBytes('build\inflate.bin'); if($f.Length -lt 16384){$pad=New-Object byte[] (16384-$f.Length); [IO.File]::WriteAllBytes('build\inflate.bin',$f+$pad); Write-Host (' inflate.bin padded: '+$f.Length+' -> 16384')}" if errorlevel 1 ( echo FAIL pad inflate && goto :err ) +echo [3a/9] Сборка cold OPL4 loader... +..\tools\sjasm\sjasmplus.exe asm\opl4_loader.asm >nul 2>&1 +if errorlevel 1 ( echo FAIL opl4_loader.asm && goto :err ) + echo [3b/9] Сборка cmdblocks.asm (sjasmplus)... ..\tools\sjasm\sjasmplus.exe asm\cmdblocks.asm >nul 2>&1 if errorlevel 1 ( echo FAIL cmdblocks.asm && goto :err ) +node scripts\overlay_bin.js build\cmdblocks.bin build\opl4_loader.bin 0x1800 +if errorlevel 1 ( echo FAIL overlay OPL4 loader && goto :err ) REM cmdblocks.asm имеет SAVEBIN "build/cmdblocks.bin" внутри echo [4/9] Компиляция C файлов... @@ -91,6 +97,9 @@ if errorlevel 1 ( echo FAIL inflate_call.s && goto :err ) sdasz80 %ASFLAGS% build\spectrum.rel asm\spectrum.s >nul if errorlevel 1 ( echo FAIL spectrum.s && goto :err ) +sdasz80 %ASFLAGS% build\cold_return.rel asm\cold_return.s >nul +if errorlevel 1 ( echo FAIL cold_return.s && goto :err ) + echo [6/9] Линковка... REM Порядок важен: crt0 первым (entry point), затем модули REM Layout: CODE #8000–#B8CF, DATA #B8D0–#BFFF @@ -108,6 +117,7 @@ sdcc -mz80 --no-std-crt0 --out-fmt-ihx --code-loc 0x8000 --data-loc %DATA_LOC% ^ build\txtlib.rel ^ build\inflate_call.rel ^ build\spectrum.rel ^ + build\cold_return.rel ^ -o build\vgmplay.ihx >nul if errorlevel 1 ( echo FAIL link && goto :err ) diff --git a/src_sdcc/inc/variant_cfg.h b/src_sdcc/inc/variant_cfg.h index 5d47420..2b8de80 100644 --- a/src_sdcc/inc/variant_cfg.h +++ b/src_sdcc/inc/variant_cfg.h @@ -35,7 +35,7 @@ /* Плотная раскладка: полная версия spectrum.s + загрузчик OPL4. * Проверяется build.bat: _CODE должен закончиться раньше _DATA, * а последняя секция — раньше 0xC000. */ -#define VARIANT_DATA_LOC 0xB8D0 +#define VARIANT_DATA_LOC 0xB8DC /* ── Спектроанализатор: скорости затухания (в TV-кадрах) ────────── */ /* Меньше = быстрее затухание. 1 = каждый кадр (~20 мс). */ diff --git a/src_sdcc/inc/vgm.h b/src_sdcc/inc/vgm.h index 94ded75..94d85f9 100644 --- a/src_sdcc/inc/vgm.h +++ b/src_sdcc/inc/vgm.h @@ -188,6 +188,7 @@ extern uint8_t vgm_chip_type; /** Массив обнаруженных чипов (для вывода информации о файле) */ extern uint8_t vgm_chip_count; +extern uint8_t vgm_opl4_preloaded_rom; extern vgm_chip_entry_t vgm_chip_list[VGM_MAX_CHIPS]; /** Адрес точки петли (#C000–#FFFF); 0 = нет петли */ diff --git a/src_sdcc/lib/vgm.c b/src_sdcc/lib/vgm.c index ae168fe..d767fa6 100644 --- a/src_sdcc/lib/vgm.c +++ b/src_sdcc/lib/vgm.c @@ -41,6 +41,7 @@ uint16_t vgm_end_addr; // смещение в окне #C000, на к uint16_t vgm_version; // версия VGM в BCD (например, 0x0151 для v1.51) uint8_t vgm_chip_type; // битовая маска типа чипов (VGM_CHIP_xxx), например VGM_CHIP_OPL2 | VGM_CHIP_AY uint8_t vgm_chip_count; // количество обнаруженных чипов (для вывода информации о файле) +uint8_t vgm_opl4_preloaded_rom; // cold loader already moved a 0x84 image vgm_chip_entry_t vgm_chip_list[VGM_MAX_CHIPS]; @@ -185,26 +186,63 @@ static const chip_scan_t chip_scan[] = { /* Пропустить n байт в VGM-потоке (арифметически, без чтения). * Используется для data block 0x67, PCM RAM write 0x68 и др. */ -static void vgm_skip(uint32_t n) +static void vgm_skip(uint32_t n) __naked { - uint16_t ptr = vgm_read_ptr; - uint32_t avail = 0x10000UL - (uint32_t)ptr; + (void)n; + __asm + ; sdcccall(1): n = HL:DE (HL high word, DE low word). + ; Convert the mapped address C000h..FFFFh to a 14-bit page offset, + ; add it to n, then split the result back into page and address. + ld bc, (_vgm_read_ptr) + ld a, b + and a, #0x3F + ld b, a - if (n < avail) { - vgm_read_ptr = ptr + (uint16_t)n; - return; - } + ex de, hl ; HL = n.low, DE = n.high + add hl, bc + ex de, hl ; DE = total.low, HL = total.high + jr nc, vsk_no_carry + inc hl +vsk_no_carry: - n -= avail; - vgm_cur_page++; + ; Remember whether at least one 16K boundary was crossed. + ld a, d + and a, #0xC0 + or a, l + or a, h + ld b, a - while (n >= 0x4000UL) { - n -= 0x4000UL; - vgm_cur_page++; - } + ; page_delta = (total >> 14) & 0xFF. + ld a, d + rlca + rlca + and a, #0x03 + ld c, a + ld a, l + add a, a + add a, a + or a, c + ld c, a - vgm_read_ptr = 0xC000 + (uint16_t)n; - wc_mngcvpl(vgm_cur_page); + ; Address within the selected page remains in total.low bits 0..13. + ld a, d + and a, #0x3F + or a, #0xC0 + ld d, a + ld (_vgm_read_ptr), de + + ld a, (_vgm_cur_page) + add a, c + ld (_vgm_cur_page), a + + ; The old C implementation remapped only after crossing a boundary. + ld a, b + or a + ret z + ld a, (_vgm_cur_page) + call _wc_mngcvpl + ret + __endasm; } /* Деление числа VGM-сэмплов на 44100 вызывается только при разборе @@ -251,7 +289,8 @@ uint8_t vgm_parse_header(void) vgm_paused = 0; vgm_chip_count = 0; vgm_chip_type = VGM_CHIP_NONE; - opl4_rom_loaded = 0; + opl4_rom_loaded = vgm_opl4_preloaded_rom; + vgm_opl4_preloaded_rom = 0; for (i = 0; i < 3; i++) spec_wave_active[i] = 0; vgm_loop_addr = 0; @@ -884,106 +923,209 @@ wuf_loop: * len = полный размер data-block (4-байтовое поле "size" из потока), * включает 8-байтовый под-заголовок выше. * ───────────────────────────────────────────────────────────────────── */ -static void wave_upload_block(void) +static void wave_upload_block(void) __naked { - /* Длина payload — прямо через fb_t32 (уже посчитан вызывающим кодом), - * без промежуточной локальной переменной — короче код (без спилла - * в стек-фрейм IX; ничто другое fb_t32 в течение этой функции не - * трогает). */ - /* Порядок записи регистров адреса — high, mid, low (как в референсном - * NedoOS-драйвере, opl4setmemoryaddress) — на реальном чипе порядок, - * похоже, важен, в отличие от того, что можно было бы предположить - * по документации эмулятора. Граница SRAM 0x200000 имеет нулевые - * младшие 2 байта, поэтому "+0x200000" — это "+0x20" к старшему - * байту адреса (предполагает startAddr < 0xE00000 — с большим - * запасом для реальных VGM). */ - asm_read_2bytes(); asm_read_2bytes(); /* ROM/RAM Size (4 байта) — не используется */ - - /* Синхронно (не через cmd-buffer) включить NEW2 в FM Bank1 (рег.#05=3): - * без этого чип игнорирует ЛЮБЫЕ обращения к wave-части, включая - * доступ к памяти. CMDBLK_INIT_OPL4 делает то же самое через ISR- - * буфер — но применится только когда реально запустится ISR, а эта - * заливка идёт синхронно ДО этого момента. Заинлайнено — вызывается - * только отсюда. Регистр #02 переключается ниже непосредственно - * перед заливкой и сразу возвращается в sound-generation mode. */ __asm + ; ROM/RAM Size (4 bytes) is informational only. + call _asm_read_2bytes + call _asm_read_2bytes + + ; NEW2: FM bank 1, register 05h = 03h. Wave registers ignore writes + ; until this bit is enabled, and the command ISR has not started yet. call _chip_wait ld a, #0x05 out (0xC6), a call _chip_wait ld a, #0x03 out (0xC7), a + + ; VGM start address is little-endian; YMF278B wants high/mid/low. + call _asm_read_byte + ld (_fb_b2), a + call _asm_read_byte + ld (_fb_b1), a + call _asm_read_byte + ld (_fb_ch), a + call _asm_read_byte ; unused top byte of the 32-bit address + + ; Patch 128 twelve-byte headers only for a ROM image beginning at 0. + xor a + ld (_hdr_recs), a + ld (_hdr_sub), a + ld a, (_fb_reg) + cp a, #0x84 + jr nz, wub_header_ready + ld a, (_fb_ch) + ld hl, #_fb_b1 + or a, (hl) + inc hl + or a, (hl) + jr nz, wub_header_ready + ld a, #0x80 + ld (_hdr_recs), a +wub_header_ready: + + ; Memory address = 200000h + start address. Register order matters. + ld a, #0x03 + call _wave_out_reg + ld a, (_fb_ch) + add a, #0x20 + call _wave_out_dat + ld a, #0x04 + call _wave_out_reg + ld a, (_fb_b1) + call _wave_out_dat + ld a, #0x05 + call _wave_out_reg + ld a, (_fb_b2) + call _wave_out_dat + + ; 11h = memory access mode, WT header base 200000h; 06h = data port. + ld a, #0x02 + call _wave_out_reg + ld a, #0x11 + call _wave_out_dat + ld a, #0x06 + call _wave_out_reg + + ; fb_t32 contains the whole block payload, including its 8-byte header. + ld hl, (_fb_t32) + ld de, #0x0008 + or a + sbc hl, de + ld (_fb_t32), hl + jr nc, wub_len_ready + ld hl, (_fb_t32 + 2) + dec hl + ld (_fb_t32 + 2), hl +wub_len_ready: + + ld a, (_hdr_recs) + or a + jr z, wub_fast_test + ld a, #0x01 + ld (_opl4_rom_loaded), a + + ; Patch address bit 21 in byte 0 of each of the first 128 headers. +wub_patch_test: + ld a, (_hdr_recs) + or a + jr z, wub_fast_test + ld hl, #_fb_t32 + ld a, (hl) + inc hl + or a, (hl) + inc hl + or a, (hl) + inc hl + or a, (hl) + jp z, wub_finish + + call _asm_read_byte + ld e, a + ld a, (_hdr_sub) + or a + jr nz, wub_patch_count + ld a, e + or a, #0x20 + ld e, a +wub_patch_count: + ld a, (_hdr_sub) + inc a + cp a, #0x0C + jr nz, wub_store_sub + xor a + ld (_hdr_sub), a + ld hl, #_hdr_recs + dec (hl) + jr wub_patch_write +wub_store_sub: + ld (_hdr_sub), a +wub_patch_write: + ld a, e + call _wave_out_dat + + ; --fb_t32. The high word changes only when the low word wraps. + ld hl, (_fb_t32) + dec hl + ld (_fb_t32), hl + ld a, h + and a, l + inc a + jr nz, wub_patch_test + ld hl, (_fb_t32 + 2) + dec hl + ld (_fb_t32 + 2), hl + jr wub_patch_test + + ; Upload the rest in chunks that never cross a mapped 16K page. +wub_fast_test: + ld hl, #_fb_t32 + ld a, (hl) + inc hl + or a, (hl) + inc hl + or a, (hl) + inc hl + or a, (hl) + jr z, wub_finish + + ; fb_w = 0 - fb_rp: number of bytes through FFFFh inclusive. + ld hl, (_fb_rp) + xor a + sub a, l + ld e, a + ld a, #0x00 + sbc a, h + ld d, a + ld (_fb_w), de + + ; If the remaining 32-bit length is smaller, use its low word. + ld hl, (_fb_t32 + 2) + ld a, h + or a, l + jr nz, wub_fast_upload + ld hl, (_fb_t32) + or a + sbc hl, de + jr nc, wub_fast_upload + ld hl, (_fb_t32) + ld (_fb_w), hl +wub_fast_upload: + call _wave_upload_fast + + ; fb_t32 -= fb_w, propagating carry into the high word. + ld hl, (_fb_t32) + ld de, (_fb_w) + or a + sbc hl, de + ld (_fb_t32), hl + jr nc, wub_after_subtract + ld hl, (_fb_t32 + 2) + dec hl + ld (_fb_t32 + 2), hl +wub_after_subtract: + + ; wave_upload_fast leaves fb_rp at zero exactly on a page boundary. + ld hl, (_fb_rp) + ld a, h + or a, l + jr nz, wub_fast_test + ld hl, #_fb_cpg + inc (hl) + ld a, (hl) + call _wc_mngcvpl + ld hl, #0xC000 + ld (_fb_rp), hl + jr wub_fast_test + +wub_finish: + ld a, #0x02 + call _wave_out_reg + ld a, #0x10 ; sound generation, WT base 200000h + call _wave_out_dat + ret __endasm; - - /* Переиспользуем существующие static (fb_ch/fb_b1/fb_b2) вместо - * локальных переменных — короче код (без спилла в стек-фрейм IX), - * ветки, где они обычно используются (AY/YM2203, чтение 2 байт), - * с этой не пересекаются. */ - /* Start Address в VGM записан в Intel byte order (little-endian). - * Регистры YMF278B #03/#04/#05, наоборот, принимают адрес как - * high/mid/low. Поэтому читаем младший байт в fb_b2, а старший из - * используемых 24 бит — в fb_ch. Прежний порядок случайно работал - * для адресов вида 0x00xx00 (например, 0x001800), но перемешивал - * почти все последующие куски ROM/RAM image. */ - fb_b2 = asm_read_byte(); /* младший байт адреса */ - fb_b1 = asm_read_byte(); /* средний байт адреса */ - fb_ch = asm_read_byte(); /* старший байт (до добавления базы SRAM) */ - asm_read_byte(); /* старший байт 32-битного поля (>16 МБ) */ - - /* Блок покрывает таблицу заголовков, только если это 0x84 (ROM Image) - * И его начало — самый старт RAM-региона (адрес 0). Именно так - * выглядят все реальные файлы этого типа (заголовки всегда идут - * первыми). Более общий случай (старт не с нуля) не поддерживаем — - * не встречался ни в одном реальном VGM. */ - hdr_recs = (fb_reg == 0x84 && (fb_ch | fb_b1 | fb_b2) == 0) ? 128 : 0; - hdr_sub = 0; - - /* Порядок записи регистров #03/#04/#05 (hi/mid/lo) — как в референсном - * NedoOS-драйвере (opl4setmemoryaddress): high, mid, low. */ - wave_out_reg(0x03); wave_out_dat(fb_ch + 0x20); /* + база SRAM (0x200000) */ - wave_out_reg(0x04); wave_out_dat(fb_b1); - wave_out_reg(0x05); wave_out_dat(fb_b2); - - /* Регистр #02: 0x11 = WT header base 0x200000 + memory access mode. - * Ранее адрес и значение были перепутаны (#11 <- #02), поэтому SRAM - * фактически не переходила в режим записи. */ - wave_out_reg(0x02); wave_out_dat(0x11); - - wave_out_reg(0x06); /* регистр данных памяти — дальше только данные, - * memaddr авто-инкрементируется самим чипом */ - fb_t32 -= 8; - - if (hdr_recs) - opl4_rom_loaded = 1; - - /* Первые 128 заголовков ROM image требуют побайтного патча адреса. */ - while (hdr_recs && fb_t32) { - uint8_t b = asm_read_byte(); - if (hdr_sub == 0) - b |= 0x20; /* взвести адресный бит 0x200000 */ - if (++hdr_sub == 12) { - hdr_sub = 0; - hdr_recs--; - } - wave_out_dat(b); - fb_t32--; - } - - /* Остаток блока — быстрыми кусками до границы VPL-страницы. */ - while (fb_t32) { - fb_w = (uint16_t)(0u - (uint16_t)fb_rp); /* байт до wrap #FFFF->#0000 */ - if (fb_t32 < fb_w) - fb_w = (uint16_t)fb_t32; - wave_upload_fast(); - fb_t32 -= fb_w; - if (!(uint16_t)fb_rp) { - fb_cpg++; - wc_mngcvpl(fb_cpg); - fb_rp = (uint8_t *)0xC000; - } - } - - wave_out_reg(0x02); wave_out_dat(0x10); /* sound generation, WT base 0x200000 */ } /* ───────────────────────────────────────────────────────────────────── diff --git a/src_sdcc/main.c b/src_sdcc/main.c index c4922b0..96e704f 100644 --- a/src_sdcc/main.c +++ b/src_sdcc/main.c @@ -101,38 +101,79 @@ extern uint8_t cfg_saa_mode; static uint8_t s_saa_mode_user; /* user setting from INI (preserved across tracks) */ static uint8_t s_board_dual_saa; /* 0=одночиповая(default), 1=двухчиповая (INI param 4) */ -static void detect_active_chips(void) +static void detect_active_chips(void) __naked { - s_has_opl = s_has_ay = s_has_ay2 = 0; - s_has_saa = s_has_saa2 = 0; - s_has_ym2203 = s_has_ym2203_2 = 0; - for (uint8_t i = 0; i < vgm_chip_count; i++) { - uint8_t id = vgm_chip_list[i].id; - uint8_t dual = vgm_chip_list[i].flags & VGM_CF_DUAL; - switch (id) { - case VGM_OFF_YM2203: - s_has_ym2203 = 1; - if (dual) s_has_ym2203_2 = 1; - /* fallthrough (проваливаемся) */ - case VGM_OFF_AY8910: - s_has_ay = 1; - if (dual) s_has_ay2 = 1; - break; - case VGM_OFF_YM3526: - case VGM_OFF_YM3812: - case VGM_OFF_Y8950: - case VGM_OFF_YMF262: - case VGM_OFF_YMF278B: - s_has_opl = 1; - break; - case VGM_OFF_SAA1099: - s_has_saa = 1; - if (dual) s_has_saa2 = 1; - break; - default: - break; - } - } + __asm + ; The seven cache bytes are declared consecutively above. + xor a + ld hl, #_s_has_opl + ld b, #7 +dac_clear: + ld (hl), a + inc hl + djnz dac_clear + + ld a, (_vgm_chip_count) + or a + ret z + ld b, a + ld de, #_vgm_chip_list + +dac_loop: + ld a, (de) ; id + ld l, a + inc de + ld a, (de) ; flags (bit 0 = dual) + ld c, a + inc de ; skip flags + clock_khz (2 bytes) + inc de + inc de + + ld a, l + cp a, #0x44 ; YM2203 + jr z, dac_ym2203 + cp a, #0x74 ; AY8910 + jr z, dac_ay + cp a, #0xC8 ; SAA1099 + jr z, dac_saa + + ; OPL ids are 50h,54h,58h,5Ch,60h: one aligned range check. + cp a, #0x50 + jr c, dac_next + cp a, #0x61 + jr nc, dac_next + and a, #0x03 + jr nz, dac_next + ld a, #1 + ld (_s_has_opl), a + jr dac_next + +dac_ym2203: + ld a, #1 + ld (_s_has_ym2203), a + bit 0, c + jr z, dac_ay + ld (_s_has_ym2203_2), a + ; YM2203 also contains an AY-compatible PSG, so fall through. +dac_ay: + ld a, #1 + ld (_s_has_ay), a + bit 0, c + jr z, dac_next + ld (_s_has_ay2), a + jr dac_next + +dac_saa: + ld a, #1 + ld (_s_has_saa), a + bit 0, c + jr z, dac_next + ld (_s_has_saa2), a + +dac_next: + djnz dac_loop + ret + __endasm; } /* cfg_loop_rewinds, cfg_min_duration, cfg_max_duration определены в vgm.c */ @@ -526,27 +567,144 @@ uint8_t drow_ui(void) /* render_static_info() удалена — заменена на drow_ui() */ /* Проверить расширение файла: .vgz / .VGZ */ -static uint8_t is_vgz_filename(void) +static uint8_t is_vgz_filename(void) __naked { - const char *p = wc_file_name; - const char *dot = 0; - while (*p) { - if (*p == '.') dot = p; - p++; - } - if (!dot) return 0; - dot++; - if ((dot[0]=='v'||dot[0]=='V') && - (dot[1]=='g'||dot[1]=='G') && - (dot[2]=='z'||dot[2]=='Z') && - dot[3]==0) - return 1; - return 0; + __asm + ld hl, (_wc_file_name) + ld de, #0 ; address of the last dot +ivf_scan: + ld a, (hl) + or a + jr z, ivf_check + cp a, #'.' + jr nz, ivf_next + ld d, h + ld e, l +ivf_next: + inc hl + jr ivf_scan + +ivf_check: + ld a, d + or a, e + jr z, ivf_no + inc de + ex de, hl + + ; ASCII upper/lower case differ by bit 5. + ld a, (hl) + or a, #0x20 + cp a, #'v' + jr nz, ivf_no + inc hl + ld a, (hl) + or a, #0x20 + cp a, #'g' + jr nz, ivf_no + inc hl + ld a, (hl) + or a, #0x20 + cp a, #'z' + jr nz, ivf_no + inc hl + ld a, (hl) + or a + jr nz, ivf_no + inc a ; return 1 + ret +ivf_no: + xor a ; return 0 + ret + __endasm; +} + +/* The unused tail of plugin-relative page 5 (not WC physical page #05) + * contains the one-shot large OPL4 loader at logical address 9800h. + * Offset 1800h is also above WC's 1000h..1447h screen scratch range. */ +static uint8_t load_large_opl4(void) __naked +{ + __asm + ld a, i + push af ; P/V remembers incoming IFF2 + di ; page 5 does not contain the resident ISR + ld a, #5 + ld hl, #0x9800 + ld de, #ll4_after_cold + push de ; #6020 is a jump, provide its return address + jp 0x6020 ; WC #6028 is position-dependent in some builds +ll4_after_cold: + ld c, a ; bit 7 = ROM, bits 0..6 = final pages + ld d, h ; keep cold-loader return across POP AF + ld e, l + pop af + jp po, ll4_irq_was_off + ei ; safe now: #6028 has restored page 0 +ll4_irq_was_off: + ld a, c + and a, #0x7f + jr z, ll4_fail + ld a, c + and a, #0x80 + ld (_vgm_opl4_preloaded_rom), a + ld a, c + and a, #0x7f + ld c, a ; final page count + ; D/E already hold page/address high byte + +ll4_tail_loop: + ld a, d + cp a, #64 + jr nc, ll4_success + + push bc + push de + ld a, d + call _wc_mngcvpl + pop de + pop bc + + xor a ; blocks up to next 16K boundary + sub a, e + srl a + ld b, a + push bc + push de + push af ; one-byte stack argument for wc_load512 + inc sp + ld h, e + ld l, #0 + call _wc_load512 + pop de + pop bc + cp a, #0x0f ; EOF: compacted stream is complete + jr z, ll4_success + or a + jr nz, ll4_fail + + inc d + ld e, #0xc0 + jr ll4_tail_loop + +ll4_success: + push bc + xor a + call _wc_mngcvpl + pop bc + ld a, c + ret + +ll4_fail: + xor a + call _wc_mngcvpl ; leave Win3 in a known state on reject + xor a + ret + __endasm; } uint8_t load_vgm(void) { uint8_t i; + uint8_t large_file = 0; uint32_t fsize = wc_file_size; /* Минимальный размер — хотя бы 256 байт */ @@ -556,6 +714,10 @@ uint8_t load_vgm(void) s_pages = (uint8_t)((fsize + 16383UL) >> 14); if (!s_pages) s_pages = 1; + if (s_pages > 64) { + s_pages = 64; + large_file = 1; + } /* vgm_end_page / vgm_end_addr устанавливаются в vgm_parse_header() из поля EOF offset заголовка VGM — здесь задавать не нужно. */ @@ -642,6 +804,12 @@ uint8_t load_vgm(void) dbg_trace2(0xA5, s_pages, 0); } + if (large_file) { + s_pages = load_large_opl4(); + if (!s_pages) + return 0; + } + return s_pages; } @@ -805,16 +973,31 @@ static void instant_abort(void) /* ── 3. Обнулить wait_ctr (отменить CMD_WAIT) ─────────── */ isr_wait_ctr = 0; - /* ── 4. Заполнить НЕАКТИВНЫЙ буфер shutdown-цепочкой ───── * - * vgm_hl_pos уже установлен на vgm_hl_abort_pos → * - * vgm_fill_buffer выдаст silence + CMD_ISR_DONE. */ + /* ── 4. Заполнить ОБА буфера shutdown-цепочкой ────────── * + * vgm_hl_pos уже установлен на vgm_hl_abort_pos. * + * Цепочка (SILENCE_OPL ≈ 384 байта + WAVE_OFF + WAVE_TL * + * + CMD_ISR_DONE) НЕ помещается в один буфер: лимит на * + * заполнение — CMD_BUF_SIZE-48 = 464 байта, а cmdblock * + * нельзя продолжить с середины — при переполнении он * + * начинается заново со следующего вызова. Поэтому * + * заполняем сразу оба буфера, пока ISR заморожен: иначе * + * после первого CMD_END_BUF он уйдёт во второй буфер со * + * СТАРЫМ содержимым и никогда не дойдёт до CMD_ISR_DONE * + * (трек зависает на месте вместо перехода к следующему). */ free_idx = (isr_active_buf & 1) ^ 1; - vgm_fill_buffer(free_idx); + vgm_fill_buffer(free_idx); /* часть 1: SILENCE_OPL + ... */ + vgm_fill_buffer(free_idx ^ 1); /* часть 2: ... + CMD_ISR_DONE */ - /* ── 5. Переключить ISR на заполненный буфер ─────────── */ + /* ── 5. Переключить ISR на первый из них ─────────────── */ isr_active_buf = free_idx; isr_read_ptr = (uint16_t)(free_idx ? cmd_buf_b : cmd_buf_a); + /* Синхронизировать учёт update_buffer(): оба буфера только что + * заполнены здесь, напрямую, минуя его собственный механизм. */ + s_last_active_buf = free_idx; + s_buf_ready[0] = 1; + s_buf_ready[1] = 1; + /* ── 6. Разморозить → ISR обработает shutdown ───────── */ isr_enabled = 1; } @@ -824,7 +1007,14 @@ void update_buffer(void) uint8_t active; uint8_t free_idx; - if (!s_playback_inited || !isr_enabled || isr_done) + /* isr_final_pending: ISR замер на CMD_FINALIZE, ждёт instant_abort() + * из главного цикла. Продолжать обычное упреждающее заполнение буфера + * здесь нельзя: vgm_hl_pos уже не совпадает с тем, что предполагает + * instant_abort() при вызове vgm_fill_buffer() для shutdown-цепочки, + * и повторное/параллельное заполнение может затереть буфер до того, + * как ISR доберётся до настоящего CMD_ISR_DONE — трек тогда никогда + * не завершается (зависает на последних тактах без перехода дальше). */ + if (!s_playback_inited || !isr_enabled || isr_done || isr_final_pending) return; active = (uint8_t)(isr_active_buf & 1U); @@ -1012,12 +1202,14 @@ void main(void) s_vgz_compressed_pages = 0; draw_pre_load_info(); - /* Если файл .vgz — показать "Unpacking..." ДО начала загрузки. * - * Это последний WC API вызов перед inflate — после него load_vgm * - * работает только через порты, без обращений к WC. */ - if (is_vgz_filename()) { + /* Показать "Loading..." ДО начала загрузки — для .vgz (распаковка) + * и для файлов >= 1 МБ (синхронная заливка сэмплов OPL4 в SRAM + * карты идёт байт за байтом с busy-wait и занимает секунды; без + * надписи выглядит как зависание). Для .vgz это последний WC API + * вызов перед inflate — дальше load_vgm работает только портами. */ + if (is_vgz_filename() || ((const uint8_t *)&wc_file_size)[2] >= 0x10) { buf_clear(work_buf); - buf_append_str(work_buf, " Unpacking..."); + buf_append_str(work_buf, " Loading..."); print_line(&s_wnd, ROW_VGM_START, work_buf, WC_COLOR(WC_YELLOW, WC_BLACK)); /* Настройка progress bar: получить адрес текст. экрана для VGM секции col 2 */ diff --git a/src_sdcc/scripts/overlay_bin.js b/src_sdcc/scripts/overlay_bin.js new file mode 100644 index 0000000..f25c13f --- /dev/null +++ b/src_sdcc/scripts/overlay_bin.js @@ -0,0 +1,14 @@ +const fs = require('fs'); + +const [targetPath, overlayPath, offsetText] = process.argv.slice(2); +if (!targetPath || !overlayPath || !offsetText) process.exit(2); + +const target = fs.readFileSync(targetPath); +const overlay = fs.readFileSync(overlayPath); +const offset = Number(offsetText); +if (!Number.isInteger(offset) || offset < 0 || offset + overlay.length > target.length) + process.exit(1); + +overlay.copy(target, offset); +fs.writeFileSync(targetPath, target); +console.log(` overlay: ${overlay.length} bytes at +0x${offset.toString(16)}`);