joeylib2/examples/spacetaxi/stRender.c

499 lines
17 KiB
C

// Space Taxi -- renderer: the simulation's screen RAM, colour RAM,
// charset and sprite frame onto the JoeyLib stage.
//
// The screen is 40x25 character cells painted from the live charset
// (the game edits glyphs at runtime: the transporter hatch, the title
// logo flip-book, the laser beams), one JoeyLib tile per glyph built
// on a scratch surface and cached by bitmap so an animated glyph is
// built once per shape. Only the cells the simulation listed as dirty
// are repainted. The eight VIC sprites are drawn from their bitmaps
// through a small cache of JoeyLib sprites keyed on (pointer, colour,
// multicolour mode), with save-under and LIFO restore. Sprite 0 has
// VIC priority, so the draw order is sprite 7 first, sprite 0 last,
// and a sprite that has not moved (and has nothing repainted under
// it) simply stays on the stage: only it and everything drawn after
// it are undrawn and redrawn when it changes.
#include <string.h>
#include "spacetaxi.h"
#define ST_SPRITE_TILES 3u
// The IIgs reaches its globals DBR-relative, so all of BSS must fit the
// entry bank below the I/O window; the caches are sized down there.
#if defined(__W65816__)
#define ST_SPRITE_CACHE 24u
#define ST_CELL_CACHE 24u
#else
#define ST_SPRITE_CACHE 72u
// Pre-coloured cells keyed direct-mapped by character + colours, so a
// full 256 covers every (char, fg, bg=black) with no collisions.
#define ST_CELL_CACHE 256u
#endif
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES)
#define ST_SPRITE_PX (8 * ST_SPRITE_TILES)
typedef struct {
jlSpriteT *sprite;
uint8_t ptr;
uint8_t color;
uint8_t multi;
uint8_t mc0;
uint8_t mc1;
uint8_t level; // level index the bitmap came from (level sprites)
bool used;
uint32_t lastUse; // for least-recently-used eviction
} StSpriteCacheT;
typedef struct {
uint8_t bits[8];
uint8_t fg;
uint8_t bg;
jlTileT tile;
bool used;
} StCellCacheT;
// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
typedef struct {
bool drawn;
uint16_t x;
uint8_t y;
uint8_t ptr;
uint8_t color;
uint8_t multi;
} StDrawnT;
typedef struct {
jlSurfaceT *scratch;
StCellCacheT cells[ST_CELL_CACHE];
uint32_t cacheStamp;
StSpriteCacheT cache[ST_SPRITE_CACHE];
jlSpriteBackupT backup[ST_HW_SPRITES];
uint8_t backupMem[ST_HW_SPRITES][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
StDrawnT slot[ST_HW_SPRITES];
uint8_t lastPresentFrame;
} StRenderStateT;
static StRenderStateT gRender;
// The VIC-II palette in register order, $0RGB.
static const uint16_t kC64Palette[16] = {
0x0000, 0x0FFF, 0x0833, 0x06BB, 0x0839, 0x05A4, 0x0438, 0x0BC7,
0x0852, 0x0540, 0x0B66, 0x0555, 0x0777, 0x09E8, 0x076C, 0x09AA
};
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
static void collectGlyphs(StSimT *sim);
static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1);
static void dropCache(void);
static void paintCells(jlSurfaceT *stage, StSimT *sim);
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell);
static bool spriteOnScreen(int16_t px, int16_t py);
// The JoeyLib sprite for hardware sprite `idx` of the current frame,
// built on first use from its VIC bitmap and colours.
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
uint8_t ptr = sim->frame.ptr[idx];
uint8_t color = sim->frame.color[idx];
uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u);
uint8_t mc0 = multi ? sim->spriteMc0 : 0u;
uint8_t mc1 = multi ? sim->spriteMc1 : 0u;
uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu;
const uint8_t *bm;
StSpriteCacheT *slot = 0;
uint8_t k;
uint8_t row;
gRender.cacheStamp++;
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
StSpriteCacheT *e = &gRender.cache[k];
if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) {
e->lastUse = gRender.cacheStamp;
return e->sprite;
}
if (slot == 0 || !e->used || (slot->used && e->used && e->lastUse < slot->lastUse)) {
if (slot == 0 || !slot->used || !e->used || e->lastUse < slot->lastUse) {
slot = e;
}
}
}
if (slot->used) {
jlSpriteDestroy(slot->sprite);
slot->sprite = 0;
slot->used = false;
}
bm = stSimSpriteBitmap(sim, ptr);
jlFillRect(gRender.scratch, 0, 0, ST_SPRITE_PX, ST_SPRITE_PX, 0u);
if (bm != 0) {
for (row = 0u; row < ST_SPRITE_H; row++) {
uint32_t bits = ((uint32_t)bm[row * 3u] << 16) | ((uint32_t)bm[row * 3u + 1u] << 8) | bm[row * 3u + 2u];
uint8_t x;
if (multi != 0u) {
for (x = 0u; x < 12u; x++) {
uint8_t pair = (uint8_t)((bits >> (22u - x * 2u)) & 3u);
uint8_t c = 0u;
if (pair == 1u) {
c = mc0;
} else if (pair == 2u) {
c = color;
} else if (pair == 3u) {
c = mc1;
}
if (c != 0u) {
jlDrawPixel(gRender.scratch, (int16_t)(x * 2u), (int16_t)row, c);
jlDrawPixel(gRender.scratch, (int16_t)(x * 2u + 1u), (int16_t)row, c);
}
}
} else {
for (x = 0u; x < 24u; x++) {
if ((bits & (1uL << (23u - x))) != 0u) {
jlDrawPixel(gRender.scratch, (int16_t)x, (int16_t)row, color);
}
}
}
}
}
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
if (slot->sprite == 0) {
return 0;
}
// Only the cab, passenger and flame move every tick; the codegen
// arena is theirs. Everything else stays interpreted (drawn once).
// Only the cab, exhaust and passenger move every tick; the codegen
// arena is theirs. Everything else stays interpreted (drawn seldom).
if (idx <= 2u) {
(void)jlSpriteCompile(slot->sprite);
}
slot->lastUse = gRender.cacheStamp;
slot->ptr = ptr;
slot->color = color;
slot->multi = multi;
slot->mc0 = mc0;
slot->mc1 = mc1;
slot->level = level;
slot->used = true;
return slot->sprite;
}
// A changed glyph (the hatch, the logo flip, the laser beams) makes
// every cell that shows it dirty; the cell cache self-invalidates on the
// bitmap compare, so no per-char tile needs rebuilding here.
static void collectGlyphs(StSimT *sim) {
uint16_t ch;
uint16_t cell;
uint8_t dirty[ST_CHARSET_CHARS];
bool any = false;
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
dirty[ch] = sim->charDirty[ch];
if (dirty[ch] != 0u) {
sim->charDirty[ch] = 0u;
any = true;
}
}
if (!any || sim->dirtyAll) {
return;
}
for (cell = 0u; cell < ST_SCREEN_CELLS; cell++) {
if (dirty[sim->screen[cell]] != 0u && sim->cellDirty[cell] == 0u) {
sim->cellDirty[cell] = 1u;
if (sim->dirtyCount < (uint8_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) {
sim->dirtyList[sim->dirtyCount++] = cell;
} else {
sim->dirtyAll = true;
return;
}
}
}
}
// Bounding box (pixels, x1/y1 exclusive) of the cells about to be
// repainted. Empty when x1 <= x0.
static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1) {
uint8_t k;
if (sim->dirtyAll) {
*x0 = 0;
*y0 = 0;
*x1 = SURFACE_WIDTH;
*y1 = SURFACE_HEIGHT;
return;
}
*x0 = SURFACE_WIDTH;
*y0 = SURFACE_HEIGHT;
*x1 = 0;
*y1 = 0;
for (k = 0u; k < sim->dirtyCount; k++) {
uint16_t cell = sim->dirtyList[k];
int16_t cx = (int16_t)((cell % ST_SCREEN_COLS) * 8u);
int16_t cy = (int16_t)((cell / ST_SCREEN_COLS) * 8u);
if (cx < *x0) {
*x0 = cx;
}
if (cy < *y0) {
*y0 = cy;
}
if ((int16_t)(cx + 8) > *x1) {
*x1 = (int16_t)(cx + 8);
}
if ((int16_t)(cy + 8) > *y1) {
*y1 = (int16_t)(cy + 8);
}
}
}
static void dropCache(void) {
uint8_t k;
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
if (gRender.cache[k].used) {
jlSpriteDestroy(gRender.cache[k].sprite);
gRender.cache[k].sprite = 0;
gRender.cache[k].used = false;
}
}
}
// Repaint the dirty cells: the list, or everything after an overflow
// or a scene change.
static void paintCells(jlSurfaceT *stage, StSimT *sim) {
uint16_t cell;
uint8_t k;
if (sim->dirtyAll) {
for (cell = 0u; cell < ST_SCREEN_CELLS; cell++) {
pasteCell(stage, sim, cell);
}
memset(sim->cellDirty, 0, ST_SCREEN_CELLS);
sim->dirtyCount = 0u;
sim->dirtyAll = false;
return;
}
for (k = 0u; k < sim->dirtyCount; k++) {
cell = sim->dirtyList[k];
pasteCell(stage, sim, cell);
sim->cellDirty[cell] = 0u;
}
sim->dirtyCount = 0u;
}
// Paste one cell through the coloured-tile cache (direct-mapped on
// character and colours; an entry is valid while its glyph bits match).
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell) {
uint8_t chr = sim->screen[cell];
uint8_t fg = sim->color[cell];
uint8_t bg = sim->bgColor;
const uint8_t *bits = sim->charset[chr];
uint8_t bx = (uint8_t)(cell % ST_SCREEN_COLS);
uint8_t by = (uint8_t)(cell / ST_SCREEN_COLS);
StCellCacheT *c = &gRender.cells[(uint16_t)(chr + fg * 61u + bg * 7u) % ST_CELL_CACHE];
uint8_t row;
uint8_t bit;
if (c->used && c->fg == fg && c->bg == bg && memcmp(c->bits, bits, 8u) == 0) {
jlTilePaste(stage, bx, by, &c->tile);
return;
}
for (row = 0u; row < 8u; row++) {
uint8_t b = bits[row];
for (bit = 0u; bit < 8u; bit++) {
jlDrawPixel(gRender.scratch, (int16_t)bit, (int16_t)row,
((b & (uint8_t)(0x80u >> bit)) != 0u) ? fg : bg);
}
}
jlTileSnap(gRender.scratch, 0u, 0u, &c->tile);
memcpy(c->bits, bits, 8u);
c->fg = fg;
c->bg = bg;
c->used = true;
jlTilePaste(stage, bx, by, &c->tile);
}
static bool spriteOnScreen(int16_t px, int16_t py) {
return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX;
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
int16_t dx0;
int16_t dy0;
int16_t dx1;
int16_t dy1;
uint8_t first = ST_HW_SPRITES;
uint8_t k;
// Find the first draw slot whose sprite changed, vanished, or sits
// over cells about to be repainted; it and every later slot are
// undrawn (last first) and redrawn below, the rest stay put.
collectGlyphs(sim);
dirtyRect(sim, &dx0, &dy0, &dx1, &dy1);
for (k = 0u; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k];
uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u);
int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN);
int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN);
bool want = (sim->frame.enableMask & (uint8_t)(1u << idx)) != 0u && spriteOnScreen(px, py);
bool same = (want == d->drawn);
if (same && want) {
same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == sim->frame.color[idx] && d->multi == multi);
}
if (same && want && dx1 > dx0) {
if (px < dx1 && (int16_t)(px + ST_SPRITE_W) > dx0 && py < dy1 && (int16_t)(py + ST_SPRITE_PX) > dy0) {
same = false;
}
}
if (!same) {
first = k;
break;
}
}
for (k = ST_HW_SPRITES; k > first; k--) {
StDrawnT *d = &gRender.slot[k - 1u];
if (d->drawn) {
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
d->drawn = false;
}
}
paintCells(stage, sim);
for (k = first; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k];
jlSpriteT *sp;
int16_t px;
int16_t py;
if ((sim->frame.enableMask & (uint8_t)(1u << idx)) == 0u) {
continue;
}
px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN);
py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN);
if (!spriteOnScreen(px, py)) {
continue;
}
sp = cachedSprite(sim, idx);
if (sp == 0) {
continue;
}
jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]);
d->drawn = true;
d->x = sim->frame.x[idx];
d->y = sim->frame.y[idx];
d->ptr = sim->frame.ptr[idx];
d->color = sim->frame.color[idx];
d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u);
}
{
// Sync to the retrace unless the frame already spans more than
// one: a late frame goes out at once rather than waiting again.
uint8_t now = (uint8_t)jlFrameCount();
if ((uint8_t)(now - gRender.lastPresentFrame) < 2u) {
jlWaitVBL();
}
jlStagePresent();
gRender.lastPresentFrame = (uint8_t)jlFrameCount();
}
}
void stRenderInit(jlSurfaceT *stage) {
uint8_t k;
memset(&gRender, 0, sizeof(gRender));
for (k = 0u; k < ST_HW_SPRITES; k++) {
gRender.backup[k].bytes = gRender.backupMem[k];
}
jlPaletteSet(stage, 0u, kC64Palette);
jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u);
jlSurfaceClear(stage, 0u);
gRender.scratch = jlSurfaceCreate();
if (gRender.scratch != 0) {
jlPaletteSet(gRender.scratch, 0u, kC64Palette);
jlScbSetRange(gRender.scratch, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u);
jlSurfaceClear(gRender.scratch, 0u);
}
}
// Build (and compile) the cels the game draws every screen -- cab,
// exhaust, passenger, wreck, warp, intro cab and star -- up front, so
// no frame pays for a sprite build mid-play. A bar on the stage shows
// progress on the slower ports.
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 };
static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 };
static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 };
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u);
uint8_t done = 0u;
uint8_t k;
StFrameT saved = sim->frame;
sim->frame.multiMask = 0x07u;
sim->spriteMc0 = 0x02u;
sim->spriteMc1 = 0x07u;
for (k = 0u; k < sizeof(kCabPtrs); k++) {
sim->frame.ptr[0] = kCabPtrs[k];
sim->frame.color[0] = 0x06u;
(void)cachedSprite(sim, 0u);
done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u);
jlStagePresent();
}
for (k = 0u; k < sizeof(kPassPtrs); k++) {
sim->frame.ptr[1] = kPassPtrs[k];
sim->frame.color[1] = 0x06u;
(void)cachedSprite(sim, 1u);
done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u);
jlStagePresent();
}
for (k = 0u; k < sizeof(kFlamePtrs); k++) {
sim->frame.ptr[2] = kFlamePtrs[k];
sim->frame.color[2] = 0x07u;
(void)cachedSprite(sim, 2u);
done++;
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u);
jlStagePresent();
}
// The intro star is a hires sprite in colour 7 (built, not compiled).
sim->frame.multiMask = 0u;
sim->frame.ptr[6] = 0xDAu;
sim->frame.color[6] = 0x07u;
(void)cachedSprite(sim, 6u);
sim->frame = saved;
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
}
// A new scene: nothing drawn is valid any more.
void stRenderSceneChanged(StSimT *sim) {
uint8_t k;
for (k = 0u; k < ST_HW_SPRITES; k++) {
gRender.slot[k].drawn = false;
}
stSimDirtyAll(sim);
memset(sim->charDirty, 1, ST_CHARSET_CHARS);
}
void stRenderShutdown(void) {
dropCache();
if (gRender.scratch != 0) {
jlSurfaceDestroy(gRender.scratch);
gRender.scratch = 0;
}
}