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 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
+103
-125
@@ -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_SprDir; // current sprite direction: SPR_NORTH/SOUTH/EAST/WEST
|
||||||
u8 g_SprFrame; // current animation frame: 0-3
|
u8 g_SprFrame; // current animation frame: 0-3
|
||||||
u8 g_VSeed; // per-game seed for tile variant hash
|
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)
|
// MAZE GENERATION (iterative DFS)
|
||||||
@@ -143,7 +150,7 @@ void GenerateMaze(void)
|
|||||||
// RENDERING
|
// 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)
|
void DrawSprite(void); // draws at (SPR_DRAW_X, SPR_DRAW_Y)
|
||||||
|
|
||||||
// Returns 1 if render-grid tile (rx, ry) is a wall.
|
// 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).
|
// 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.
|
// 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)
|
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();
|
VDP_CommandWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +208,7 @@ void DrawTile(u8 tx, u8 ty, u8 col)
|
|||||||
if (col == COL_START) DrawTileData(tx, ty, TILE_W * 2);
|
if (col == COL_START) DrawTileData(tx, ty, TILE_W * 2);
|
||||||
else if (col == COL_EXIT) DrawTileData(tx, ty, TILE_W * 3);
|
else if (col == COL_EXIT) DrawTileData(tx, ty, TILE_W * 3);
|
||||||
else {
|
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();
|
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).
|
// 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.
|
// 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 seg;
|
||||||
u8 localDir;
|
u8 localDir;
|
||||||
@@ -303,14 +329,13 @@ void DrawSpriteAt(u16 spx, u16 spy)
|
|||||||
SET_BANK_SEGMENT(2, seg);
|
SET_BANK_SEGMENT(2, seg);
|
||||||
const u8 *fr = (const u8*)0x8000 + ((u16)localDir * 4 + g_SprFrame) * (SPR_W * SPR_H);
|
const u8 *fr = (const u8*)0x8000 + ((u16)localDir * 4 + g_SprFrame) * (SPR_W * SPR_H);
|
||||||
for (u8 row = 0; row < SPR_H; row++) {
|
for (u8 row = 0; row < SPR_H; row++) {
|
||||||
u16 rowaddr = (spy + row) * 256u;
|
|
||||||
const u8 *rowpx = fr + (u16)row * SPR_W;
|
const u8 *rowpx = fr + (u16)row * SPR_W;
|
||||||
u8 col = 0;
|
u8 col = 0;
|
||||||
while (col < SPR_W) {
|
while (col < SPR_W) {
|
||||||
if (rowpx[col] == 0) { col++; continue; }
|
if (rowpx[col] == 0) { col++; continue; }
|
||||||
u8 start = col;
|
u8 start = col;
|
||||||
while (col < SPR_W && rowpx[col] != 0) 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);
|
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
|
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;
|
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);
|
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])
|
row_buf[c] = (sr < SPR_H && fr[(u16)sr * SPR_W + c])
|
||||||
? fr[(u16)sr * SPR_W + c] : bg;
|
? 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);
|
SET_BANK_SEGMENT(2, prevSeg);
|
||||||
}
|
}
|
||||||
@@ -494,7 +519,7 @@ static void DrawArrow(u8 ax, u8 ay, u8 dir)
|
|||||||
u8 bits = g_ArrowBits[dir][row];
|
u8 bits = g_ArrowBits[dir][row];
|
||||||
for (u8 col = 0; col < 8; col++)
|
for (u8 col = 0; col < 8; col++)
|
||||||
row_buf[col] = (bits & (0x80u >> col)) ? 0xFC : 0x00;
|
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.
|
// Wrap-around (torus) scroll: animate the hardware scroll register from the
|
||||||
// Returns the keyboard row 8 sampled at the final animation frame so the
|
// resting base to base±64, rendering the newly exposed edge into the VRAM cells
|
||||||
// caller can chain the next move immediately if a direction is still held.
|
// 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)
|
u8 ScrollDraw(i8 dx, i8 dy)
|
||||||
{
|
{
|
||||||
// Render the new edge strip into off-screen VRAM (STAGING_Y) first, while the
|
// Render the new edge strip into off-screen staging (STAGING_Y) first, while
|
||||||
// sprite is still visible — staging writes are off-screen so the user sees nothing.
|
// the sprite is still visible — staging writes are off-screen, invisible.
|
||||||
PreRenderStrip(dx, dy);
|
PreRenderStrip(dx, dy);
|
||||||
|
|
||||||
// Vertical scroll wraps within the 256-row VRAM page. At maximum animation offset
|
// Resting scroll offset before this move (multiple of 64).
|
||||||
// (R#23=64 for dy=1, R#23=192 for dy=-1) the display wraps into VRAM rows that
|
u8 base = (dy != 0) ? g_BaseY : g_BaseX;
|
||||||
// were never rendered by normal tile rendering (7 rows × 32px = 224px only).
|
|
||||||
//
|
// Vertical only has 32px of slack in the 256-row torus, so of the two new
|
||||||
// At R#23=64 (dy=1), display row N reads VRAM row (N+64)%256:
|
// tile-rows exactly one is already off-screen (in the slack just past the
|
||||||
// display rows 160..191 → VRAM 224..255 → fill with staging tile row 5 (STAGING_Y)
|
// viewport) and can be filled now; the other overlaps on-screen VRAM and is
|
||||||
// display rows 192..211 → VRAM 0.. 19 → fill with staging tile row 6 (STAGING_Y+32)
|
// deferred into the loop. At base=0 these reduce to the original rows 224/0
|
||||||
// but rows 0..31 are VISIBLE at R#23=0 (display rows 0..31), so they must be filled
|
// (dy=1) and 224/192 (dy=-1). Staging holds the new tiles at rows 0 / +32.
|
||||||
// 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.
|
|
||||||
if (dy == 1) {
|
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) {
|
} 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).
|
// 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).
|
// 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));
|
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_vx = SPR_DRAW_X;
|
||||||
u8 spr_vy = SPR_DRAW_Y;
|
u8 spr_vy = SPR_DRAW_Y;
|
||||||
|
|
||||||
if (dy != 0) {
|
if (dy != 0) {
|
||||||
// Vertical: R#23 (MSX2).
|
// Vertical: R#23 (MSX2). Offset runs base → base±64.
|
||||||
for (u8 i = 0; i < ANIM_STEPS; i++) {
|
for (u8 i = 0; i < ANIM_STEPS; i++) {
|
||||||
Halt();
|
Halt();
|
||||||
u8 new_vy = (u8)((i16)SPR_DRAW_Y + (i16)dy * g_AO[i]);
|
u8 new_vy = (u8)((i16)SPR_DRAW_Y + (i16)dy * g_AO[i]);
|
||||||
g_SprFrame = (g_SprFrame + 1) & 3;
|
g_SprFrame = (g_SprFrame + 1) & 3;
|
||||||
DrawSpriteCompV(spr_vx, spr_vy, new_vy, old_ox, old_oy);
|
DrawSpriteCompV(spr_vx, spr_vy, new_vy, old_ox, old_oy);
|
||||||
spr_vy = new_vy;
|
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
|
// Deferred fill of the second new tile-row, once it has scrolled off.
|
||||||
// range that wraps into view at the later animation steps.
|
// 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).
|
||||||
// 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.
|
|
||||||
if (dy == 1 && i == 4) {
|
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) {
|
} 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 {
|
} else {
|
||||||
// Horizontal: R#26/R#27 (MSX2+).
|
// Horizontal: R#26/R#27 (MSX2+). The 8-tile viewport is the full 256px
|
||||||
// VRAM is exactly 256px wide = display width; any non-zero offset wraps
|
// width, so the entering edge overwrites the leaving edge in place as it
|
||||||
// the opposite edge into view. Progressive fill: at each step, HMMM only
|
// scrolls off. Progressive fill: each step HMMMs only the delta columns
|
||||||
// the delta columns that just became safe (newly wrapped into the entering
|
// that just became safe. DrawSpriteCompH writes with the CPU (WriteVV),
|
||||||
// side), started at VBlank before sprite draw. DrawSpriteCompH uses
|
// running in parallel with the background HMMM; CommandWait before the next
|
||||||
// VDP_WriteVRAM only, so it runs safely in parallel with the background HMMM.
|
// Halt() ensures the fill completes within the VBlank it started.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Staging layout (both directions): PreRenderStrip always writes the new
|
// Entering-edge VRAM columns (base-relative):
|
||||||
// 2-column strip to staging x=0..63. Mapping to the entering-side VRAM:
|
// dx= 1: leaving-left cols = VRAM (base+prev)..; staging col prev → there.
|
||||||
// dx= 1: entering side = x=0..offset-1; staging col j → VRAM x=j
|
// dx=-1: entering-left cols = VRAM (base+192+fs)..; staging col fs → there.
|
||||||
// dx=-1: entering side = x=offset..255; staging col j → VRAM x=192+j
|
|
||||||
// (so staging srcX = j = col - 192, dest = 192 + srcX)
|
|
||||||
u8 prev = 0;
|
u8 prev = 0;
|
||||||
for (u8 i = 0; i < ANIM_STEPS; i++) {
|
for (u8 i = 0; i < ANIM_STEPS; i++) {
|
||||||
Halt();
|
Halt();
|
||||||
u8 cur = g_AO[i];
|
u8 cur = g_AO[i];
|
||||||
u8 w = cur - prev;
|
u8 w = cur - prev;
|
||||||
if (dx == 1) {
|
if (dx == 1) {
|
||||||
VDP_CommandHMMM(prev, STAGING_Y, prev, 0, w, 224);
|
VDP_CommandHMMM(prev, STAGING_Y, (u8)(base + prev), 0, w, 224);
|
||||||
} else {
|
} else {
|
||||||
u8 fs = (u8)(64u - cur);
|
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);
|
u8 new_vx = (u8)((i16)SPR_DRAW_X + (i16)dx * (i16)cur);
|
||||||
g_SprFrame = (g_SprFrame + 1) & 3;
|
g_SprFrame = (g_SprFrame + 1) & 3;
|
||||||
DrawSpriteCompH(spr_vx, new_vx, spr_vy, old_ox, old_oy);
|
DrawSpriteCompH(spr_vx, new_vx, spr_vy, old_ox, old_oy);
|
||||||
spr_vx = new_vx;
|
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();
|
VDP_CommandWait();
|
||||||
prev = cur;
|
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);
|
u8 row8 = Keyboard_Read(8);
|
||||||
g_Joy = Joystick_Read(JOY_PORT_1);
|
g_Joy = Joystick_Read(JOY_PORT_1);
|
||||||
Halt();
|
|
||||||
|
|
||||||
// Hide the display while we reorganise VRAM — avoids showing the partial
|
// Commit the new resting base. The newly exposed edge is already in VRAM (from
|
||||||
// shift/paste. The screen goes black briefly then the complete frame appears.
|
// the fills), the old content is already at the correct torus position, and the
|
||||||
VDP_EnableDisplay(FALSE);
|
// 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
|
||||||
// Reset offset to 0 BEFORE shifting VRAM (bounce fix: see memory notes).
|
// register was already set to the new base by the last animation step.
|
||||||
if (dy != 0)
|
if (dy != 0)
|
||||||
VDP_SetVerticalOffset(0);
|
g_BaseY = (u8)(base + ((dy == 1) ? 64u : (u8)(0u - 64u)));
|
||||||
else
|
else
|
||||||
VDP_SetHorizontalOffset(0);
|
g_BaseX = (u8)(base + ((dx == 1) ? 64u : (u8)(0u - 64u)));
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
return row8;
|
return row8;
|
||||||
}
|
}
|
||||||
@@ -1003,6 +968,12 @@ void main(void)
|
|||||||
// behaviour the horizontal path in ScrollDraw() relies on.
|
// behaviour the horizontal path in ScrollDraw() relies on.
|
||||||
VDP_SetHorizontalMode(R25_SP2);
|
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.
|
// Software sprite: initialize direction and frame for this game.
|
||||||
g_SprDir = SPR_SOUTH; // face south (toward exit) at game start
|
g_SprDir = SPR_SOUTH; // face south (toward exit) at game start
|
||||||
g_SprFrame = 0;
|
g_SprFrame = 0;
|
||||||
@@ -1088,6 +1059,10 @@ void main(void)
|
|||||||
// M key or joystick button A: show minimap while held
|
// 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)) {
|
if (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A)) {
|
||||||
VDP_EnableDisplay(FALSE);
|
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();
|
DrawMinimap();
|
||||||
VDP_EnableDisplay(TRUE);
|
VDP_EnableDisplay(TRUE);
|
||||||
// Wait for M and joy-A both released — one read per VBlank (Halt() syncs
|
// 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);
|
row4 = Keyboard_Read(4);
|
||||||
g_Joy = Joystick_Read(JOY_PORT_1);
|
g_Joy = Joystick_Read(JOY_PORT_1);
|
||||||
} while (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A));
|
} 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_EnableDisplay(FALSE);
|
||||||
|
VDP_SetVerticalOffset(g_BaseY);
|
||||||
|
VDP_SetHorizontalOffset(g_BaseX);
|
||||||
DrawFull();
|
DrawFull();
|
||||||
VDP_EnableDisplay(TRUE);
|
VDP_EnableDisplay(TRUE);
|
||||||
row8 = 0xFF;
|
row8 = 0xFF;
|
||||||
|
|||||||
Reference in New Issue
Block a user