From 9faff1cfba82693177484bd3a825466a573e11cf Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Fri, 3 Jul 2026 21:15:55 +0200 Subject: [PATCH] Fix music tempo, note-on-switch, and title scroll shift Three follow-up fixes to the per-screen music: - Decode music in the H_TIMI VBlank interrupt (MusicVBlank) instead of once per Halt() in the game loops. The heavy per-step VDP work in ScrollDraw could overrun a VBlank, dropping the decode rate so the music slowed only while moving; the ISR keeps tempo steady regardless. The playing track is mapped into the unused bank-3 window (0xA000) and left there for the screen, so the ISR reads it with no bank switching and never races the main code's bank-2 tile/sprite swaps. PlayTrack() switches tracks under DI/EI. The game loops revert to plain Halt()s. - PlayTrack() mutes the OPLL (key-off all 9 channels) before starting the next track, silencing the note that lingered across screen changes. - TitleScreen()/ShowHints() zero the hardware scroll offset before drawing their absolute-coord images. Gameplay rests at g_BaseX/g_BaseY in {0,64,128,192}; without the reset the returning title/hints screen appeared horizontally shifted (only after finishing on a non-zero base). Co-Authored-By: Claude Opus 4.8 --- projects/mazegame/CLAUDE.md | 26 +++++--- projects/mazegame/emul/rom/mazegame.rom | Bin 262144 -> 262144 bytes projects/mazegame/mazegame.c | 85 +++++++++++++----------- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/projects/mazegame/CLAUDE.md b/projects/mazegame/CLAUDE.md index df76ba3..cebaee3 100644 --- a/projects/mazegame/CLAUDE.md +++ b/projects/mazegame/CLAUDE.md @@ -82,16 +82,26 @@ so `./build.sh run` gives openMSX the FM-PAC (`-ext fmpac`). The PSG carries no regenerate after replacing a `.lvgm`, re-run the small embed step (bin2c-style: dump the bytes as `const u8 g_Mus*[N]` in the matching `s{26..29}_b2.c`). See `~/Repositories/msx-music-generator` (the generator + its `msxgl-example/README.md`). -- **Playback protocol (banking):** because the tracks live in the bank-2 window and gameplay - is constantly switching segment 2 for tiles/sprites, two helpers in `mazegame.c` bracket - every player call with a save/restore of the current segment: `PlayTrack(seg)` banks the - track in and calls `LVGM_Play((const u8*)0x8000, TRUE)`; `PlayFrame()` banks the current - track's segment (`g_MusSeg`) in, calls `LVGM_Decode()`, and restores. **Every `Halt()` in a - wait/animation loop is paired with a `PlayFrame()`** so the track advances at 60 Hz — - including inside `ScrollDraw`, the gameplay input loops (which were made frame-synced for - this), and `WaitKeySequence`. `MSXMusic_Initialize()` runs once at the top of `main()`. +- **Decode runs in the VBlank interrupt (this is the key design point).** `MusicVBlank()` is + installed as the `H_TIMI` hook (`BIOS_SetHookCallback`) and calls `LVGM_Decode()` once per + VBlank — so the tempo is **steady regardless of how long a frame's game work takes**. (An + earlier design decoded once per `Halt()` in the main loop; the heavy per-step VDP work in + `ScrollDraw` could overrun a VBlank, so the decode rate — and the music — slowed *only while + moving*. The ISR decouples them.) The game loops therefore contain **no** decode calls; they + are plain `Halt()`/busy-waits as before the music existed. +- **Banking — why it's race-free:** the tracks are compiled at the bank-2 link address but at + runtime `PlayTrack()` maps the current track into the **bank-3 window** (`0xA000-0xBFFF`) and + leaves it there for the whole screen. Gameplay only ever switches **bank 2** (`0x8000`) for + tiles/sprites, so bank 3 is never touched and the ISR can read `0xA000` at any moment without + a bank save/restore and without racing the main code. `PlayTrack(seg)` runs under + `DisableInterrupt()`/`EnableInterrupt()` (mute the old track's hanging note → map bank 3 → + `LVGM_Play(0xA000, TRUE)`) so `MusicVBlank` can't decode mid-switch. `MSXMusic_Initialize()` + and the hook install run once at the top of `main()`; `MusicVBlank` is a no-op until a track + is playing (`LVGM_IsPlaying()`). - **Config:** `LibModules` adds `psg`, `msx-music`, `vgm/lvgm_player`; `msxgl_config.h` sets `LVGM_USE_PSG`/`LVGM_USE_MSXMUSIC` on (rest off) and `PSG_ACCESS = PSG_DIRECT`. +- The tracks are authored at **60 Hz**; on a 50 Hz (PAL) machine they play a constant ~17 % + slower (the decode rate is the display refresh). That's uniform, not movement-specific. - The boot **logo animation is intentionally silent** (no track). ### VRAM layout (off-screen caches above the visible 256×212) diff --git a/projects/mazegame/emul/rom/mazegame.rom b/projects/mazegame/emul/rom/mazegame.rom index 6d60f1fa2fa9a1bcec7a3769fde4b742092643e9..992573cae41d9fec9bc27eda5518edd9d12ba90b 100644 GIT binary patch delta 7250 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 F|38yQ#kv3h diff --git a/projects/mazegame/mazegame.c b/projects/mazegame/mazegame.c index b6984d4..42a69a3 100644 --- a/projects/mazegame/mazegame.c +++ b/projects/mazegame/mazegame.c @@ -7,6 +7,7 @@ //----------------------------------------------------------------------------- #include "msxgl.h" #include "rom_mapper.h" +#include "bios_hook.h" #include "psg.h" #include "msx-music.h" #include "vgm/lvgm_player.h" @@ -102,37 +103,40 @@ u8 g_BaseY; // resting vertical scroll offset (mult //============================================================================= // MUSIC (lVGM, MSX-Music / OPLL) — one looping track per screen, in banked ROM //============================================================================= -// Each track fills the start of its own bank-2 segment (0x8000 window). Only one -// plays at a time; LVGM_Decode advances a pointer inside that window, so the -// track's segment must be banked in around every decode. Each track is < 8KB, so -// it never crosses a segment boundary. PlayTrack/PlayFrame save & restore the -// caller's bank-2 segment, so the music never disturbs the tile/sprite banking -// that gameplay drawing relies on. +// Each track fills the start of its own ROM segment (the `_b2` files are compiled +// at the bank-2 link address, but at runtime the track is mapped into the *bank-3* +// window at 0xA000-0xBFFF and left there for the whole screen). Gameplay only ever +// switches bank 2 (0x8000) for tiles/sprites, so bank 3 is never disturbed and the +// track stays put. Decoding runs in the H_TIMI VBlank interrupt (MusicVBlank), so +// the music tempo is rock-steady no matter how long a frame's game work takes — the +// scroll animation can overrun a VBlank without dragging the music down with it. +// Because the ISR reads only from bank 3, it never races the main code's bank-2 +// switching, and needs no bank save/restore. Each track is < 8KB (one segment). #define MUS_SEG_TITLE 26 // mazegame_s26_b2.c (g_MusTitle) #define MUS_SEG_HINTS 27 // mazegame_s27_b2.c (g_MusHints) #define MUS_SEG_MAZE 28 // mazegame_s28_b2.c (g_MusMaze) #define MUS_SEG_WIN 29 // mazegame_s29_b2.c (g_MusWin) -u8 g_MusSeg; // bank-2 segment of the currently playing track +#define MUS_WINDOW ((const u8*)0xA000) // bank-3 window the track is mapped into -// Start a looping track: bank in its segment, begin playback, remember the segment. -static void PlayTrack(u8 seg) +// H_TIMI (VBlank) hook: advance the current track one frame. No-op until a track is +// playing. Reads only from the bank-3 window, which nothing else touches. +void MusicVBlank(void) { - u8 prev = GET_BANK_SEGMENT(2); - SET_BANK_SEGMENT(2, seg); - LVGM_Play((const u8*)0x8000, TRUE); - g_MusSeg = seg; - SET_BANK_SEGMENT(2, prev); + if (LVGM_IsPlaying()) + LVGM_Decode(); } -// Advance the current track by one frame. Safe with any bank-2 segment selected; -// the caller's segment is restored on return. Call once per VBlank (after Halt()). -static void PlayFrame(void) +// Switch to a looping track: silence the previous track's lingering note (key-off all +// 9 FM channels), map the new track into bank 3, and start it. Interrupts are disabled +// across the switch so MusicVBlank can't decode from a half-updated pointer or segment. +static void PlayTrack(u8 seg) { - u8 prev = GET_BANK_SEGMENT(2); - SET_BANK_SEGMENT(2, g_MusSeg); - LVGM_Decode(); - SET_BANK_SEGMENT(2, prev); + DisableInterrupt(); + MSXMusic_Mute(); + SET_BANK_SEGMENT(3, seg); + LVGM_Play(MUS_WINDOW, TRUE); + EnableInterrupt(); } //============================================================================= @@ -640,7 +644,6 @@ u8 ScrollDraw(i8 dx, i8 dy) // Vertical: R#23 (MSX2). Offset runs base → base±64. for (u8 i = 0; i < ANIM_STEPS; i++) { Halt(); - PlayFrame(); u8 new_vy = (u8)((i16)SPR_DRAW_Y + (i16)dy * g_AO[i]); g_SprFrame = (g_SprFrame + 1) & 3; DrawSpriteCompV(spr_vx, spr_vy, new_vy, old_ox, old_oy); @@ -670,7 +673,6 @@ u8 ScrollDraw(i8 dx, i8 dy) u8 prev = 0; for (u8 i = 0; i < ANIM_STEPS; i++) { Halt(); - PlayFrame(); u8 cur = g_AO[i]; u8 w = cur - prev; if (dx == 1) { @@ -873,11 +875,9 @@ static u8 AnyInput(void) // Drain held inputs, wait for a fresh press, then wait for release. static void WaitKeySequence(void) { - // Halt()+PlayFrame() keeps the current track advancing (60 Hz) while waiting, - // and syncs one keyboard read per VBlank. Callers (hints/win) have music playing. - while (AnyInput()) { Halt(); PlayFrame(); } - while (!AnyInput()) { Halt(); PlayFrame(); } - while (AnyInput()) { Halt(); PlayFrame(); } + while (AnyInput()); + while (!AnyInput()); + while (AnyInput()); } // Show the hints screen image (SCREEN 8) before the game begins. @@ -886,6 +886,9 @@ static void WaitKeySequence(void) static void ShowHints(void) { VDP_EnableDisplay(FALSE); + // Drawn at absolute VRAM coords — clear any leftover hardware scroll offset. + VDP_SetVerticalOffset(0); + VDP_SetHorizontalOffset(0); for (u8 y = 0; y < IMG_ROWS; y++) WriteImgRow(IMG_SEG_HINTS, y); VDP_EnableDisplay(TRUE); @@ -901,6 +904,12 @@ void TitleScreen(void) VDP_EnableDisplay(FALSE); VDP_ClearVRAM(); + // This image is drawn at absolute VRAM coords, so clear any hardware scroll + // offset left over from a previous game (gameplay rests at g_BaseX/g_BaseY in + // {0,64,128,192}); without this the returning title screen appears shifted. + VDP_SetVerticalOffset(0); + VDP_SetHorizontalOffset(0); + // Stream image A (no title) to VRAM display area for (u8 y = 0; y < IMG_ROWS; y++) WriteImgRow(IMG_SEG_A, y); @@ -909,7 +918,7 @@ void TitleScreen(void) PlayTrack(MUS_SEG_TITLE); // Hold image A for ~2 seconds (100 VBlanks at 50Hz) - for (u8 i = 0; i < 100; i++) { Halt(); PlayFrame(); } + for (u8 i = 0; i < 100; i++) Halt(); // 16-step scanline interleave dissolve to image B (with title). // Step S updates every 16th row starting at row S. @@ -917,12 +926,12 @@ void TitleScreen(void) for (u8 step = 0; step < 16; step++) { for (u8 y = step; y < IMG_ROWS; y += 16) WriteImgRow(IMG_SEG_B, y); - Halt(); PlayFrame(); Halt(); PlayFrame(); Halt(); PlayFrame(); + Halt(); Halt(); Halt(); } - // Wait for any key or joystick input, then release (music keeps playing) - while (!AnyInput()) { Halt(); PlayFrame(); } - while (AnyInput()) { Halt(); PlayFrame(); } + // Wait for any key or joystick input, then release + while (!AnyInput()); + while (AnyInput()); } //============================================================================= @@ -986,8 +995,10 @@ static void ShowWinScreen(void) void main(void) { - // Init the MSX-Music (OPLL) chip once, before any track plays. + // Init the MSX-Music (OPLL) chip once, then install the VBlank hook that decodes + // the current track every frame (MusicVBlank is a no-op until a track is playing). MSXMusic_Initialize(); + BIOS_SetHookCallback(H_TIMI, MusicVBlank); ShowLogoAnimation(); @@ -1077,8 +1088,6 @@ void main(void) if (!hasDir) { // Wait for all arrow keys and joystick direction released (debounce) do { - Halt(); - PlayFrame(); UpdateTimer(); row8 = Keyboard_Read(8); g_Joy = Joystick_Read(JOY_PORT_1); @@ -1088,8 +1097,6 @@ void main(void) // Wait for an arrow key, joystick direction, C (cheat), M/joy-A (minimap), S (cat sense) u8 row3, row4, row5; do { - Halt(); - PlayFrame(); UpdateTimer(); row8 = Keyboard_Read(8); row3 = Keyboard_Read(3); @@ -1123,7 +1130,6 @@ void main(void) // past the BIOS interrupt that rewrites the keyboard row selector). do { Halt(); - PlayFrame(); row4 = Keyboard_Read(4); g_Joy = Joystick_Read(JOY_PORT_1); } while (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A)); @@ -1143,7 +1149,6 @@ void main(void) DrawArrow(108, 108, GetExitDir()); do { Halt(); - PlayFrame(); row5 = Keyboard_Read(5); } while (IS_KEY_PRESSED(row5, KEY_S)); // Restore: redraw tile under player then redraw sprite