diff --git a/projects/mazegame/emul/rom/mazegame.rom b/projects/mazegame/emul/rom/mazegame.rom index d0ab6d8..f856439 100644 Binary files a/projects/mazegame/emul/rom/mazegame.rom and b/projects/mazegame/emul/rom/mazegame.rom differ diff --git a/projects/mazegame/mazegame.c b/projects/mazegame/mazegame.c index 92d6bb0..191f987 100644 --- a/projects/mazegame/mazegame.c +++ b/projects/mazegame/mazegame.c @@ -88,6 +88,13 @@ u8 g_Joy; // joystick state sampled at end of anima u8 g_SprDir; // current sprite direction: SPR_NORTH/SOUTH/EAST/WEST u8 g_SprFrame; // current animation frame: 0-3 u8 g_VSeed; // per-game seed for tile variant hash +// Persistent scroll base (torus/wrap-around scrolling). The visible page is a +// 256x256 VRAM torus; g_BaseX/g_BaseY are the resting hardware scroll offset +// (R#26/27 and R#23) and are always a multiple of 64 in {0,64,128,192}. +// Screen pixel (sx,sy) maps to VRAM ((sx+g_BaseX)&255, (sy+g_BaseY)&255). +// Scrolling never resets/bulk-shifts VRAM, so there is no per-move blank/flash. +u8 g_BaseX; // resting horizontal scroll offset (mult of 64) +u8 g_BaseY; // resting vertical scroll offset (mult of 64) //============================================================================= // MAZE GENERATION (iterative DFS) @@ -143,7 +150,7 @@ void GenerateMaze(void) // RENDERING //============================================================================= -void DrawSpriteAt(u16 spx, u16 spy); // forward declaration — defined after DrawFull +void DrawSpriteAt(u8 spx, u8 spy); // forward declaration — defined after DrawFull void DrawSprite(void); // draws at (SPR_DRAW_X, SPR_DRAW_Y) // Returns 1 if render-grid tile (rx, ry) is a wall. @@ -170,11 +177,29 @@ u8 IsWall(i8 rx, i8 ry) } } +// Write a horizontal pixel run to SCREEN coords (sx,sy), mapping through the +// scroll-base torus and splitting at the 256-column boundary if the run wraps. +// sy is one row; vertical torus wrap is handled by the &255 on the row index. +static void WriteVV(const u8 *buf, u8 sx, u8 sy, u8 len) +{ + u8 px = (u8)(sx + g_BaseX); + u16 rowaddr = (u16)(u8)(sy + g_BaseY) * 256u; + if ((u16)px + len <= 256u) { + VDP_WriteVRAM(buf, rowaddr + px, 0, len); + } else { + u8 first = (u8)(256u - px); + VDP_WriteVRAM(buf, rowaddr + px, 0, first); // up to col 255 + VDP_WriteVRAM(buf + first, rowaddr, 0, (u8)(len - first)); // wrap to col 0, same row + } +} + // Blit a preloaded tile from the VRAM cache area (y=224) to screen tile (tx, ty). // srcX=0 for TILE_WALL, srcX=32 for TILE_FLOOR. Uses HMMM — fast, no per-row overhead. +// Destination goes through the scroll-base torus. Tiles are 32-aligned and the +// base is a multiple of 64, so a tile never straddles the 256 wrap boundary. void DrawTileData(u8 tx, u8 ty, u8 srcX) { - VDP_CommandHMMM(srcX, TILE_CACHE_Y, tx * TILE_W, ty * TILE_W, TILE_W, TILE_W); + VDP_CommandHMMM(srcX, TILE_CACHE_Y, (u8)(tx * TILE_W + g_BaseX), (u8)(ty * TILE_W + g_BaseY), TILE_W, TILE_W); VDP_CommandWait(); } @@ -183,7 +208,7 @@ void DrawTile(u8 tx, u8 ty, u8 col) if (col == COL_START) DrawTileData(tx, ty, TILE_W * 2); else if (col == COL_EXIT) DrawTileData(tx, ty, TILE_W * 3); else { - VDP_CommandHMMV(tx * TILE_W, ty * TILE_W, TILE_W, TILE_W, col); + VDP_CommandHMMV((u8)(tx * TILE_W + g_BaseX), (u8)(ty * TILE_W + g_BaseY), TILE_W, TILE_W, col); VDP_CommandWait(); } } @@ -285,10 +310,11 @@ void DrawFull(void) } -// Draw the current sprite frame at (spx, spy) in VRAM. +// Draw the current sprite frame at SCREEN position (spx, spy). // Sprite data lives in banked ROM segments 16 (N/S) and 17 (E/W). // Pixels of value 0x00 are transparent; contiguous opaque runs use one VDP call. -void DrawSpriteAt(u16 spx, u16 spy) +// Writes go through the scroll-base torus via WriteVV. +void DrawSpriteAt(u8 spx, u8 spy) { u8 seg; u8 localDir; @@ -303,14 +329,13 @@ void DrawSpriteAt(u16 spx, u16 spy) SET_BANK_SEGMENT(2, seg); const u8 *fr = (const u8*)0x8000 + ((u16)localDir * 4 + g_SprFrame) * (SPR_W * SPR_H); for (u8 row = 0; row < SPR_H; row++) { - u16 rowaddr = (spy + row) * 256u; const u8 *rowpx = fr + (u16)row * SPR_W; u8 col = 0; while (col < SPR_W) { if (rowpx[col] == 0) { col++; continue; } u8 start = col; while (col < SPR_W && rowpx[col] != 0) col++; - VDP_WriteVRAM(rowpx + start, rowaddr + spx + start, 0, col - start); + WriteVV(rowpx + start, (u8)(spx + start), (u8)(spy + row), col - start); } } SET_BANK_SEGMENT(2, prevSeg); @@ -361,7 +386,7 @@ static void DrawSpriteCompH(u8 vx_prev, u8 vx_new, u8 vy, i8 ox, i8 oy) u8 sc = ax - vx_new; // wraps to >SPR_W 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;