From 7c24daaf14661d0e374ab31452d39339e26348d5 Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Wed, 1 Jul 2026 16:02:39 +0200 Subject: [PATCH] Eliminate per-move scroll flash with wrap-around (torus) scrolling Movement previously ended each step by blanking the display, resetting the scroll offset to 0, bulk-HMMM-shifting the whole viewport back to a canonical origin, and pasting the new strip. That blank was visible as a flash on every move. Treat the 256x256 VRAM page as a torus instead. New g_BaseX/g_BaseY hold the resting hardware scroll offset (a multiple of 64); a move animates the scroll register from base to base +/- 64 and fills the newly exposed edge into the VRAM cells that scroll off the opposite side, then keeps the new offset as the resting base. No offset reset, no bulk shift, no strip paste, no blank -- so no flash. All fill targets are base-relative; at base 0 they reduce exactly to the previous code, and the fill timing is base-invariant. Route all gameplay VRAM drawing (tiles, sprite, compositors, cat-sense arrow) through the base via a new WriteVV helper that splits writes at the 256-column wrap. The minimap overlay draws at absolute coords, so it zeroes the offset while shown and restores the base on exit. Co-Authored-By: Claude Opus 4.8 --- projects/mazegame/emul/rom/mazegame.rom | Bin 262144 -> 262144 bytes projects/mazegame/mazegame.c | 228 +++++++++++------------- 2 files changed, 103 insertions(+), 125 deletions(-) diff --git a/projects/mazegame/emul/rom/mazegame.rom b/projects/mazegame/emul/rom/mazegame.rom index d0ab6d8896ab8214b49266d057476194dae43491..f8564398eb7a2ea6d0c840b9c8dd41c494535313 100644 GIT binary patch delta 6810 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 MDVF@YSbF1s0EBmiTL1t6 delta 7096 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 when ax < vx_new row_buf[c] = (sc < SPR_W && sp_r[sc]) ? sp_r[sc] : bg; } - VDP_WriteVRAM(row_buf, (u16)(vy + row) * 256u + vx_lo, 0, span); + WriteVV(row_buf, vx_lo, (u8)(vy + row), span); } SET_BANK_SEGMENT(2, prevSeg); } @@ -400,7 +425,7 @@ static void DrawSpriteCompV(u8 vx, u8 vy_prev, u8 vy_new, i8 ox, i8 oy) row_buf[c] = (sr < SPR_H && fr[(u16)sr * SPR_W + c]) ? fr[(u16)sr * SPR_W + c] : bg; } - VDP_WriteVRAM(row_buf, (u16)abs_y * 256u + vx, 0, SPR_W); + WriteVV(row_buf, vx, abs_y, SPR_W); } SET_BANK_SEGMENT(2, prevSeg); } @@ -494,7 +519,7 @@ static void DrawArrow(u8 ax, u8 ay, u8 dir) u8 bits = g_ArrowBits[dir][row]; for (u8 col = 0; col < 8; col++) row_buf[col] = (bits & (0x80u >> col)) ? 0xFC : 0x00; - VDP_WriteVRAM(row_buf, (u16)(ay + row) * 256u + ax, 0, 8); + WriteVV(row_buf, ax, (u8)(ay + row), 8); } } @@ -524,39 +549,38 @@ static void PreRenderStrip(i8 dx, i8 dy) } } -// Animate scroll, then HMMM-shift VRAM, place pre-rendered strip. -// Returns the keyboard row 8 sampled at the final animation frame so the -// caller can chain the next move immediately if a direction is still held. +// Wrap-around (torus) scroll: animate the hardware scroll register from the +// resting base to base±64, rendering the newly exposed edge into the VRAM cells +// that scroll off the opposite side. Nothing is bulk-shifted or blanked +// afterwards — the new offset simply becomes the resting base, so there is no +// per-move flash. Returns keyboard row 8 sampled at the final animation frame so +// the caller can chain the next move immediately if a direction is still held. +// +// The 256×256 VRAM page is a torus: content is never moved, only the scroll +// offset changes and the two newly-revealed tile-rows/cols are filled in. Fill +// targets are base-relative; the *timing* of each fill (which animation step is +// safe) is base-invariant — see the per-fill comments. All VRAM row/col targets +// are computed as u8 so they wrap within the page automatically. u8 ScrollDraw(i8 dx, i8 dy) { - // Render the new edge strip into off-screen VRAM (STAGING_Y) first, while the - // sprite is still visible — staging writes are off-screen so the user sees nothing. + // Render the new edge strip into off-screen staging (STAGING_Y) first, while + // the sprite is still visible — staging writes are off-screen, invisible. PreRenderStrip(dx, dy); - // Vertical scroll wraps within the 256-row VRAM page. At maximum animation offset - // (R#23=64 for dy=1, R#23=192 for dy=-1) the display wraps into VRAM rows that - // were never rendered by normal tile rendering (7 rows × 32px = 224px only). - // - // At R#23=64 (dy=1), display row N reads VRAM row (N+64)%256: - // display rows 160..191 → VRAM 224..255 → fill with staging tile row 5 (STAGING_Y) - // display rows 192..211 → VRAM 0.. 19 → fill with staging tile row 6 (STAGING_Y+32) - // but rows 0..31 are VISIBLE at R#23=0 (display rows 0..31), so they must be filled - // only after step 4 (R#23=40), when they scroll off-screen (display rows 216+). - // - // At R#23=192 (dy=-1), display row N reads VRAM row (N+192)%256: - // display rows 0.. 31 → VRAM 192..223 → fill with staging tile row 0 (STAGING_Y) - // display rows 32.. 63 → VRAM 224..255 → fill with staging tile row 1 (STAGING_Y+32) - // but rows 192..223 are VISIBLE at R#23=0 (display rows 192..211 on-screen), so they - // must be filled only after step 3 (R#23=228), when all rows are off-screen (220+). - // - // The first copy (rows 224..255) is safe here because those rows are always off-screen - // at R#23=0 (display only shows rows 0..211). The second copy is deferred into the - // animation loop (see below). Both ranges are safe to modify pre-final: the - // post-animation HMMM shift overwrites them before the display comes back on. + // Resting scroll offset before this move (multiple of 64). + u8 base = (dy != 0) ? g_BaseY : g_BaseX; + + // Vertical only has 32px of slack in the 256-row torus, so of the two new + // tile-rows exactly one is already off-screen (in the slack just past the + // viewport) and can be filled now; the other overlaps on-screen VRAM and is + // deferred into the loop. At base=0 these reduce to the original rows 224/0 + // (dy=1) and 224/192 (dy=-1). Staging holds the new tiles at rows 0 / +32. if (dy == 1) { - VDP_CommandHMMM(0, STAGING_Y, 0, 224, 256, 32); VDP_CommandWait(); + // New bottom tile-row 5 → VRAM row (base+224) (off-screen slack). Staging row 0. + VDP_CommandHMMM(0, STAGING_Y, 0, (u8)(base + 224u), 256, 32); VDP_CommandWait(); } else if (dy == -1) { - VDP_CommandHMMM(0, STAGING_Y + 32, 0, 224, 256, 32); VDP_CommandWait(); + // New top tile-row 1 → VRAM row (base-32)=(base+224). Staging row 1. + VDP_CommandHMMM(0, STAGING_Y + 32, 0, (u8)(base + 224u), 256, 32); VDP_CommandWait(); } // old_ox/old_oy: viewport origin BEFORE this move (g_MX/g_MY already incremented). @@ -569,133 +593,74 @@ u8 ScrollDraw(i8 dx, i8 dy) // destination tile (g_MX/g_MY already point to the new cell at this point). DrawTileData(3, 3, GetTileSrcX(old_ox + 3, old_oy + 3)); + // Virtual (base-0) sprite position; WriteVV in the compositors adds the base. u8 spr_vx = SPR_DRAW_X; u8 spr_vy = SPR_DRAW_Y; if (dy != 0) { - // Vertical: R#23 (MSX2). + // Vertical: R#23 (MSX2). Offset runs base → base±64. for (u8 i = 0; i < ANIM_STEPS; i++) { Halt(); 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); spr_vy = new_vy; - VDP_SetVerticalOffset((dy == 1) ? g_AO[i] : (u8)(256 - g_AO[i])); + VDP_SetVerticalOffset((u8)(base + ((dy == 1) ? g_AO[i] : (u8)(0u - g_AO[i])))); - // Deferred wrap-area fill: copy the remaining staging row into the VRAM - // range that wraps into view at the later animation steps. - // - // dy=1: after step 4 (R#23=40), VRAM rows 0..31 are at display rows - // 216..247 (all off-screen). Fill with new tile row 6 so that - // steps 5..7 (R#23=50,58,64) show correct content at the bottom. - // - // dy=-1: after step 3 (R#23=228), VRAM rows 192..223 are at display rows - // 220..251 (all off-screen). Fill with new tile row 0 so that - // steps 4..7 (R#23=216,206,198,192) show correct content at top. + // Deferred fill of the second new tile-row, once it has scrolled off. + // dy=1: new bottom tile-row 6 → VRAM row base (safe after step 4). + // dy=-1: new top tile-row 0 → VRAM row (base-64)=(base+192) (safe after step 3). if (dy == 1 && i == 4) { - VDP_CommandHMMM(0, STAGING_Y + 32, 0, 0, 256, 32); VDP_CommandWait(); + VDP_CommandHMMM(0, STAGING_Y + 32, 0, base, 256, 32); VDP_CommandWait(); } else if (dy == -1 && i == 3) { - VDP_CommandHMMM(0, STAGING_Y, 0, 192, 256, 32); VDP_CommandWait(); + VDP_CommandHMMM(0, STAGING_Y, 0, (u8)(base + 192u), 256, 32); VDP_CommandWait(); } } } else { - // Horizontal: R#26/R#27 (MSX2+). - // VRAM is exactly 256px wide = display width; any non-zero offset wraps - // the opposite edge into view. Progressive fill: at each step, HMMM only - // the delta columns that just became safe (newly wrapped into the entering - // side), started at VBlank before sprite draw. DrawSpriteCompH uses - // VDP_WriteVRAM only, so it runs safely in parallel with the background HMMM. - // CommandWait before the next Halt() ensures the fill completes within the - // same VBlank it started. Each delta is at most 12px × 224 rows ≈ 1ms, - // well within the 6.4ms VBlank budget. + // Horizontal: R#26/R#27 (MSX2+). The 8-tile viewport is the full 256px + // width, so the entering edge overwrites the leaving edge in place as it + // scrolls off. Progressive fill: each step HMMMs only the delta columns + // that just became safe. DrawSpriteCompH writes with the CPU (WriteVV), + // running in parallel with the background HMMM; CommandWait before the next + // Halt() ensures the fill completes within the VBlank it started. // - // Staging layout (both directions): PreRenderStrip always writes the new - // 2-column strip to staging x=0..63. Mapping to the entering-side VRAM: - // dx= 1: entering side = x=0..offset-1; staging col j → VRAM x=j - // dx=-1: entering side = x=offset..255; staging col j → VRAM x=192+j - // (so staging srcX = j = col - 192, dest = 192 + srcX) + // Entering-edge VRAM columns (base-relative): + // dx= 1: leaving-left cols = VRAM (base+prev)..; staging col prev → there. + // dx=-1: entering-left cols = VRAM (base+192+fs)..; staging col fs → there. u8 prev = 0; for (u8 i = 0; i < ANIM_STEPS; i++) { Halt(); u8 cur = g_AO[i]; u8 w = cur - prev; if (dx == 1) { - VDP_CommandHMMM(prev, STAGING_Y, prev, 0, w, 224); + VDP_CommandHMMM(prev, STAGING_Y, (u8)(base + prev), 0, w, 224); } else { u8 fs = (u8)(64u - cur); - VDP_CommandHMMM(fs, STAGING_Y, (u16)(fs + 192u), 0, w, 224); + VDP_CommandHMMM(fs, STAGING_Y, (u8)(base + 192u + fs), 0, w, 224); } u8 new_vx = (u8)((i16)SPR_DRAW_X + (i16)dx * (i16)cur); g_SprFrame = (g_SprFrame + 1) & 3; DrawSpriteCompH(spr_vx, new_vx, spr_vy, old_ox, old_oy); spr_vx = new_vx; - VDP_SetHorizontalOffset((dx == 1) ? (u16)cur : (u16)(u8)(0u - cur)); + VDP_SetHorizontalOffset((u16)(u8)(base + ((dx == 1) ? cur : (u8)(0u - cur)))); VDP_CommandWait(); prev = cur; } } - // Read keyboard and joystick at the final animation frame. + // Read keyboard and joystick at the final animation frame (for chaining). u8 row8 = Keyboard_Read(8); g_Joy = Joystick_Read(JOY_PORT_1); - Halt(); - // Hide the display while we reorganise VRAM — avoids showing the partial - // shift/paste. The screen goes black briefly then the complete frame appears. - VDP_EnableDisplay(FALSE); - - // Reset offset to 0 BEFORE shifting VRAM (bounce fix: see memory notes). + // Commit the new resting base. The newly exposed edge is already in VRAM (from + // the fills), the old content is already at the correct torus position, and the + // sprite is centred (final composite). There is nothing to reorganise: no bulk + // shift, no strip paste, no display blank — hence no flash. The hardware scroll + // register was already set to the new base by the last animation step. if (dy != 0) - VDP_SetVerticalOffset(0); + g_BaseY = (u8)(base + ((dy == 1) ? 64u : (u8)(0u - 64u))); else - VDP_SetHorizontalOffset(0); - - // No explicit sprite erase here: the bulk HMMM below naturally moves the - // sprite pixels from their final animation position to the centre (e.g. - // for dx=+1 the sprite at VRAM col 164 shifts to col 100 = SPR_DRAW_X). - // DrawSprite() at the end then refreshes the final frame in place. - - // Bulk HMMM: shift existing VRAM content in the direction of movement. - // Left/up: destination < source, no overlap. Right/down: use DIX_LEFT/DIY_UP. - if (dx == 1) { - // Move right: shift VRAM left 64px. Copy cols 64..255 → 0..191 (7 rows = 224px). - VDP_CommandHMMM(64, 0, 0, 0, 192, 224); - VDP_CommandWait(); - } else if (dx == -1) { - // Move left: shift VRAM right 64px. Copy cols 0..191 → 64..255 (7 rows = 224px). - VDP_CommandHMMM_Arg(191, 0, 255, 0, 192, 224, VDP_ARG_DIX_LEFT); - VDP_CommandWait(); - } else if (dy == 1) { - // Move down: shift VRAM up 64px. Copy rows 64..223 → 0..159 (5 tile rows = 160px). - VDP_CommandHMMM(0, 64, 0, 0, 256, 160); - VDP_CommandWait(); - } else { - // Move up: shift VRAM down 64px. Copy rows 0..159 → 64..223 (160px). - VDP_CommandHMMM_Arg(0, 159, 0, 223, 256, 160, VDP_ARG_DIY_UP); - VDP_CommandWait(); - } - - // Place pre-rendered strip from staging into its final screen position. - // One HMMM per move — the entire strip appears at once, no tile-by-tile flash. - if (dx == 1) { VDP_CommandHMMM(0, STAGING_Y, 192, 0, 64, 224); VDP_CommandWait(); } - else if (dx == -1) { VDP_CommandHMMM(0, STAGING_Y, 0, 0, 64, 224); VDP_CommandWait(); } - else if (dy == 1) { VDP_CommandHMMM(0, STAGING_Y, 0, 160, 256, 64); VDP_CommandWait(); } - else { VDP_CommandHMMM(0, STAGING_Y, 0, 0, 256, 64); VDP_CommandWait(); } - - // Redraw markers (bulk HMMM may have shifted one into the non-strip area). - i8 ox = (i8)(2*g_MX) - 2; - i8 oy = (i8)(2*g_MY) - 2; - DrawMarkers(ox, oy); - - // Redraw sprite on top of the freshly placed tiles. - DrawSprite(); - - // Wait for VBlank before re-enabling display: ensures the scan is at line 0 - // when the display comes back on. Without this, VDP_EnableDisplay(TRUE) fires - // mid-frame — lines already scanned that frame remain black, looking like an - // artifact in the top half of the screen. - Halt(); - VDP_EnableDisplay(TRUE); + g_BaseX = (u8)(base + ((dx == 1) ? 64u : (u8)(0u - 64u))); return row8; } @@ -1003,6 +968,12 @@ void main(void) // behaviour the horizontal path in ScrollDraw() relies on. VDP_SetHorizontalMode(R25_SP2); + // Torus scroll base starts at 0 (no scroll offset) for the initial frame. + g_BaseX = 0; + g_BaseY = 0; + VDP_SetVerticalOffset(0); + VDP_SetHorizontalOffset(0); + // Software sprite: initialize direction and frame for this game. g_SprDir = SPR_SOUTH; // face south (toward exit) at game start g_SprFrame = 0; @@ -1088,6 +1059,10 @@ void main(void) // M key or joystick button A: show minimap while held if (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A)) { VDP_EnableDisplay(FALSE); + // Minimap is drawn at absolute VRAM (0,0); show it with zero + // scroll offset regardless of the current torus base. + VDP_SetVerticalOffset(0); + VDP_SetHorizontalOffset(0); DrawMinimap(); VDP_EnableDisplay(TRUE); // Wait for M and joy-A both released — one read per VBlank (Halt() syncs @@ -1097,8 +1072,11 @@ void main(void) 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)); - // Restore game view (DrawFull includes DrawSprite) + // Restore game view (DrawFull includes DrawSprite). Put the + // scroll offset back to the current torus base first. VDP_EnableDisplay(FALSE); + VDP_SetVerticalOffset(g_BaseY); + VDP_SetHorizontalOffset(g_BaseX); DrawFull(); VDP_EnableDisplay(TRUE); row8 = 0xFF;