From d676190aafb9340d454a9f3025515a495d42b7fc Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Sat, 19 Jan 2019 15:50:50 -0500 Subject: [PATCH] Optimize scrolling to not redraw tiles when not needed --- testbed.c | 98 ++++++++++++++++++++++++++++++++++++---------------- testbed.exe | Bin 64140 -> 65012 bytes 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/testbed.c b/testbed.c index 61cf5a5..1059b1a 100755 --- a/testbed.c +++ b/testbed.c @@ -391,7 +391,7 @@ int tifLoad(FILE *f, TifImageMeta_t meta, unsigned int *planeBuf, int maxY, int unsigned int *rp = gp + planeStride; unsigned int *ip = rp + planeStride; unsigned int *mp = ip + planeStride; - unsigned char bv, gv, rv, iv; + unsigned int bv, gv, rv, iv; if (meta.width > MAX_WIDTH || (meta.width % 16) != 0 || planes < 4 || planes > 5) { return 0; @@ -412,7 +412,7 @@ int tifLoad(FILE *f, TifImageMeta_t meta, unsigned int *planeBuf, int maxY, int int gpair = (pixelpair & 0x02) >> 1 | (pixelpair & 0x20) >> 4; int rpair = (pixelpair & 0x04) >> 2 | (pixelpair & 0x40) >> 5; int ipair = (pixelpair & 0x08) >> 3 | (pixelpair & 0x80) >> 6; - int shift = (3 - (ipixelpair % 4)) << 1; + int shift = (7 - (ipixelpair % 8)) << 1; bv |= bpair << shift; gv |= gpair << shift; @@ -425,7 +425,8 @@ int tifLoad(FILE *f, TifImageMeta_t meta, unsigned int *planeBuf, int maxY, int *rp++ = rv; *ip++ = iv; if (planes == 5) { - *mp++ = bv & gv & rv & iv; + iv = ~(bv & gv & rv & iv); + *mp++ = iv; } bv = gv = rv = iv = 0; } @@ -448,6 +449,18 @@ int tifLoad(FILE *f, TifImageMeta_t meta, unsigned int *planeBuf, int maxY, int /*** T I L E S ***/ +// Tiles are 16x16 bitmaps, stored as arrays of words. +// Each tile has 4 or 5 planes (depending on whether it is a tile or sprite) +// which are stored adjacant to each other; ie. a 16-word array of blue, +// followed by a 16-word array of green, etc. +// Tiles in RAM are stored byte-swapped to aid in fast bit-shifting, and must +// be byte-swapped before being written to video memory. + +// Because bit-shifting operations happen on little-endian words: +// 01234567 89ABCDEF << 3 => 34567XXX BCDEF012 +// which is wrong. So instead we do: +// 89ABCDEF 01234567 << 3 => BCDEFXXX 3456789A byteswap => 3456789A BCDEFXXX + #define PAGE_TILES_W 21 #define PAGE_TILES_H 13 #define PAGE_TILES_COUNT (PAGE_TILES_H * PAGE_TILES_W) @@ -503,6 +516,7 @@ void loadMap(unsigned char *map, unsigned int w, unsigned int h) { screen.map = map; screen.w = w; screen.h = h; + memset(screen.dirty, D_BGTILE, PAGE_TILES_COUNT * 2); } int prepareBuffer(int pageX, int pageY) { @@ -523,6 +537,7 @@ int prepareBuffer(int pageX, int pageY) { void drawSpriteToBuf(unsigned int *sprite, int pageX, int pageY, int shift, int yStart) { unsigned int *buf, *mask; + unsigned int maskval; int y, h, plane; if (pageX < 0 || pageY < 0 || pageX >= PAGE_TILES_W || pageY >= PAGE_TILES_H || @@ -532,19 +547,20 @@ void drawSpriteToBuf(unsigned int *sprite, int pageX, int pageY, int shift, int } buf = screen.buffer[prepareBuffer(pageX, pageY)]; -/* if (yStart < 0) { + if (yStart < 0) { sprite = &sprite[-yStart]; h = yStart + 16; } else { buf = &buf[yStart]; h = 16 - yStart; } - mask = &sprite[16 * 4]; + mask = &sprite[BUF_WSTRIDE * 4]; if (shift < 0) { shift = -shift; for (plane = 0; plane < 4; plane ++) { for (y = 0; y < h; y ++) { - buf[y] = (buf[y] & ~(mask[y] << shift)) | (sprite[y] << shift); + maskval = mask[y] << shift; + buf[y] = (buf[y] & ~maskval) | ((sprite[y] << shift) & maskval); } sprite += BUF_WSTRIDE; buf += BUF_WSTRIDE; @@ -552,12 +568,13 @@ void drawSpriteToBuf(unsigned int *sprite, int pageX, int pageY, int shift, int } else { for (plane = 0; plane < 4; plane ++) { for (y = 0; y < h; y ++) { - buf[y] = (buf[y] & ~(mask[y] >> shift)) | (sprite[y] >> shift); + maskval = mask[y] >> shift; + buf[y] = (buf[y] & ~maskval) | ((sprite[y] >> shift) & maskval); } sprite += BUF_WSTRIDE; buf += BUF_WSTRIDE; } - }*/ + } } void drawSprite(unsigned int *sprite, int x, int y) { @@ -572,15 +589,32 @@ void drawSprite(unsigned int *sprite, int x, int y) { drawSpriteToBuf(sprite, pageX + 1, pageY + 1, pageOffsetX - 16, pageOffsetY - 16); } -void scroll(int x, int y) { - x = min(max(x, 0), (screen.w << 4) - 320); - y = min(max(y, 0), (screen.h << 4) - 176); - if ((screen.scrollX & 0xfff0) != (x & 0xfff0) || - (screen.scrollY & 0xfff0) != (y & 0xfff0)) { - memset(screen.dirty, D_BGTILE, 2 * PAGE_TILES_COUNT); +void scroll(int newX, int newY) { + newX = min(max(newX, 0), (screen.w << 4) - 320); + newY = min(max(newY, 0), (screen.h << 4) - 176); + if ((screen.scrollX & 0xfff0) != (newX & 0xfff0) || + (screen.scrollY & 0xfff0) != (newY & 0xfff0)) { + int mapX, mapY; + unsigned char page; + for (page = 0; page < 2; page ++) { + int mapOffsetOld = (screen.scrollX >> 4) + ((screen.scrollY >> 4) * screen.w); + int mapOffsetNew = (newX >> 4) + ((newY >> 4) * screen.w); + unsigned char *dirty = screen.dirty[page]; + for (mapY = 0; mapY < PAGE_TILES_H; mapY ++) { + for (mapX = 0; mapX < PAGE_TILES_W; mapX ++) { + if (*dirty != D_NOTHING || + screen.map[mapOffsetOld + mapX] != screen.map[mapOffsetNew + mapX]) { + *dirty = D_BGTILE; + } + dirty ++; + } + mapOffsetNew += screen.w; + mapOffsetOld += screen.w; + } + } } - screen.scrollX = x; - screen.scrollY = y; + screen.scrollX = newX; + screen.scrollY = newY; } void drawScreen() { @@ -591,7 +625,7 @@ void drawScreen() { unsigned int drawOffset = screen.pageOffset[screen.currentPage]; unsigned int scrollOffset = drawOffset + (offsetX >> 3) + (offsetY * PAGE_STRIDE); unsigned char *dirty = screen.dirty[screen.currentPage]; - unsigned int x, y, di, plane; + unsigned int x, y, di, plane, bmp; setAllPlanes(); setWriteMode(1); @@ -600,7 +634,9 @@ void drawScreen() { for (y = startY; y < startY + PAGE_TILES_H; y ++) { for (x = startX; x < startX + PAGE_TILES_W; x ++) { if (dirty[di++] == D_BGTILE) { - blitTile(screen.tilesOffset + screen.map[x + (y * screen.w)], drawOffset); + blitTile( + screen.tilesOffset + (screen.map[x + (y * screen.w)] << 5), + drawOffset); } drawOffset += 2; } @@ -612,7 +648,8 @@ void drawScreen() { for (di = screen.firstBuffer; di != screen.nextBuffer; di = nextBufferIndex(di)) { drawOffset = screen.bufferOffset[di] >> 1; for (y = 0; y < 16; y ++) { - WVID[drawOffset] = screen.buffer[di][y + (BUF_WSTRIDE * plane)]; + bmp = screen.buffer[di][y + (BUF_WSTRIDE * plane)]; + WVID[drawOffset] = (bmp << 8) | (bmp >> 8); drawOffset += PAGE_STRIDE >> 1; } } @@ -630,9 +667,10 @@ void drawScreen() { #define NUM_TILES 128 #define NUM_SPRITES 64 #define OFF_TILES 0x4840 - -unsigned int tiles[NUM_TILES][4*16]; -unsigned int sprites[NUM_SPRITES][5*16]; +#define TILE_STRIDE 64 +#define SPRITE_STRIDE 80 +unsigned int tiles[NUM_TILES * TILE_STRIDE]; +unsigned int sprites[NUM_SPRITES * SPRITE_STRIDE]; unsigned char map[10000]; @@ -643,7 +681,7 @@ void fillMap() { for (y = 0; y < 100; y ++) { for (x = 0; x < 100; x ++) { - map[x + (y * 100)] = (((x + y + z) >> 2) % 4) << 5; + map[x + (y * 100)] = ((x + y + z) >> 2) % 4; } } } @@ -665,8 +703,8 @@ void game_init() { f = fopen("TILES.TIF", "rb"); meta = tifLoadMeta(f); - tifLoadEGA(f, meta, OFF_TILES, NUM_TILES * 16, 16); tifLoad(f, meta, tiles, NUM_TILES * 16, 16, 4); + tifLoadEGA(f, meta, OFF_TILES, NUM_TILES * 16, 16); fclose(f); f = fopen("SPRITE.TIF", "rb"); @@ -691,13 +729,13 @@ int main() { while (!keyPressed(K_ESC)) { - if (keyPressed(K_LEFT)) x -= 1; - if (keyPressed(K_RIGHT)) x += 1; - if (keyPressed(K_UP)) y -= 1; - if (keyPressed(K_DOWN)) y += 1; + if (keyPressed(K_LEFT)) x -= 3; + if (keyPressed(K_RIGHT)) x += 3; + if (keyPressed(K_UP)) y -= 3; + if (keyPressed(K_DOWN)) y += 3; scroll(x - 152, y - 90); - drawSprite(sprites[0], 50, 50); - drawSprite(sprites[1], x, y); + drawSprite(&sprites[0 * SPRITE_STRIDE], 50, 50); + drawSprite(&sprites[1 * SPRITE_STRIDE], x, y); drawScreen(); z++; diff --git a/testbed.exe b/testbed.exe index b39e2c7af6e1ea5aef266039b6789a2210ee8245..78bdec18acd310fb44215ef6d939dc1c693745d8 100755 GIT binary patch delta 6240 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 delta 5420 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 A)Bpeg