628 lines
23 KiB
C
628 lines
23 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). A glyph is one mono JoeyLib tile
|
|
// (set bits = non-zero pixels), cached by shape and coloured on the way
|
|
// to the stage by jlTilePasteMono from the cell's colour RAM, so a
|
|
// colour change never rebuilds a tile and an animated glyph is built
|
|
// once per shape. Only the cells the simulation listed as dirty are
|
|
// repainted; a changed glyph dirties the cells showing it, found
|
|
// inside the cell range each character was last painted in. 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"
|
|
#include "stCels.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. The
|
|
// sprite cache must still hold every cel stRenderPrewarm builds (the
|
|
// cab, exhaust and passenger sets plus the intro star) or a cel change
|
|
// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped
|
|
// by character, so the count is a power of two.
|
|
#if defined(__W65816__)
|
|
#define ST_SPRITE_CACHE 48u
|
|
#define ST_GLYPH_CACHE 64u
|
|
#else
|
|
#define ST_SPRITE_CACHE 72u
|
|
#define ST_GLYPH_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)
|
|
// glyphFirst[] value for a character shown in no cell.
|
|
#define ST_NO_CELL 0xFFFFu
|
|
|
|
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;
|
|
bool pinned; // prewarmed moving cel: never evicted (its
|
|
// compiled code is costly to re-emit on the 65816)
|
|
uint32_t lastUse; // for least-recently-used eviction
|
|
} StSpriteCacheT;
|
|
|
|
|
|
// 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;
|
|
// Glyphs as mono tiles (set bits = non-zero pixels, coloured per cell
|
|
// by jlTilePasteMono), direct-mapped by character and valid while
|
|
// the bits match. Parallel arrays: power-of-two element sizes keep
|
|
// the 65816 indexing to shifts.
|
|
uint8_t glyphBits[ST_GLYPH_CACHE][8];
|
|
jlTileT glyphTiles[ST_GLYPH_CACHE];
|
|
bool glyphUsed[ST_GLYPH_CACHE];
|
|
// Cell range (inclusive) each character has been painted in since
|
|
// the last full repaint: where a changed glyph's cells can be.
|
|
uint16_t glyphFirst[ST_CHARSET_CHARS];
|
|
uint16_t glyphLast[ST_CHARSET_CHARS];
|
|
uint32_t cacheStamp;
|
|
StSpriteCacheT cache[ST_SPRITE_CACHE];
|
|
StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last
|
|
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;
|
|
// Set across stRenderPrewarm so a cel built there is pinned in the cache.
|
|
static bool gPrewarming;
|
|
|
|
// 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 void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
|
|
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
|
|
static bool loadPrecompiledCels(void);
|
|
static uint8_t cellRow(uint16_t cell);
|
|
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 const jlTileT *glyphTile(const StSimT *sim, uint8_t chr);
|
|
static void paintCells(jlSurfaceT *stage, StSimT *sim);
|
|
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by);
|
|
static bool spriteOnScreen(int16_t px, int16_t py);
|
|
static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
|
|
|
|
|
|
// Paint a VIC sprite bitmap onto the scratch surface's top-left 3x3
|
|
// tiles from the shared cel expansion (stCels.c, the same one the
|
|
// offline baker uses), so a cel built here for the interpreter fallback
|
|
// matches the precompiled .spc byte for byte.
|
|
static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) {
|
|
uint8_t blob[ST_CEL_BYTES];
|
|
jlTileT tile;
|
|
uint8_t tx;
|
|
uint8_t ty;
|
|
|
|
stCelBlob(bm, multi, color, mc0, mc1, blob);
|
|
for (ty = 0u; ty < ST_SPRITE_TILES; ty++) {
|
|
for (tx = 0u; tx < ST_SPRITE_TILES; tx++) {
|
|
tileFromChunky(&tile, &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]);
|
|
jlTilePaste(gRender.scratch, tx, ty, &tile);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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;
|
|
StSpriteCacheT *slot = 0;
|
|
StSpriteCacheT *e = gRender.lastHit[idx];
|
|
uint8_t k;
|
|
|
|
gRender.cacheStamp++;
|
|
// Most frames a hardware sprite shows the cel it showed last time.
|
|
if (e != 0 && 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;
|
|
}
|
|
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
|
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;
|
|
gRender.lastHit[idx] = e;
|
|
return e->sprite;
|
|
}
|
|
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the
|
|
// costly-to-recompile ones and are never evicted; the least-
|
|
// recently-used unpinned slot is the victim, a free slot first.
|
|
if (e->pinned) {
|
|
continue;
|
|
}
|
|
if (slot == 0 || !e->used || (slot->used && e->lastUse < slot->lastUse)) {
|
|
slot = e;
|
|
}
|
|
}
|
|
if (slot == 0) {
|
|
// Every slot is pinned (never happens with the prewarm set sized
|
|
// below the cache): reuse the queried entry's own slot is unsafe,
|
|
// so fail into the interpreter path.
|
|
return 0;
|
|
}
|
|
if (slot->used) {
|
|
jlSpriteDestroy(slot->sprite);
|
|
slot->sprite = 0;
|
|
slot->used = false;
|
|
}
|
|
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1);
|
|
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
|
|
if (slot->sprite == 0) {
|
|
return 0;
|
|
}
|
|
// 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;
|
|
slot->pinned = gPrewarming;
|
|
gRender.lastHit[idx] = slot;
|
|
return slot->sprite;
|
|
}
|
|
|
|
|
|
// Block row of a screen cell without a division: cell / 40 is
|
|
// (cell / 8) / 5, and x * 205 / 1024 equals x / 5 for every x below 125.
|
|
static uint8_t cellRow(uint16_t cell) {
|
|
return (uint8_t)(((uint16_t)(cell >> 3) * 205u) >> 10);
|
|
}
|
|
|
|
|
|
// A changed glyph (the hatch, the logo flip, the laser beams) makes
|
|
// every cell that shows it dirty. The glyph cache self-invalidates on
|
|
// the bitmap compare, so nothing is rebuilt here; the search covers
|
|
// only the cell range the character was last painted in.
|
|
static void collectGlyphs(StSimT *sim) {
|
|
uint16_t ch;
|
|
uint16_t cell;
|
|
uint16_t last;
|
|
|
|
for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) {
|
|
if (sim->charDirty[ch] == 0u) {
|
|
continue;
|
|
}
|
|
sim->charDirty[ch] = 0u;
|
|
if (sim->dirtyAll) {
|
|
continue;
|
|
}
|
|
last = gRender.glyphLast[ch];
|
|
for (cell = gRender.glyphFirst[ch]; cell <= last; cell++) {
|
|
if (sim->screen[cell] != (uint8_t)ch || sim->cellDirty[cell] != 0u) {
|
|
continue;
|
|
}
|
|
sim->cellDirty[cell] = 1u;
|
|
if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) {
|
|
sim->dirtyList[sim->dirtyCount++] = cell;
|
|
} else {
|
|
sim->dirtyAll = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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) {
|
|
uint16_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];
|
|
uint8_t by = cellRow(cell);
|
|
int16_t cy = (int16_t)(by * 8u);
|
|
int16_t cx = (int16_t)((cell - ((uint16_t)by << 5) - ((uint16_t)by << 3)) * 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The mono tile for character `chr`, rebuilt when the glyph's bits no
|
|
// longer match the cached shape (direct-mapped by character).
|
|
static const jlTileT *glyphTile(const StSimT *sim, uint8_t chr) {
|
|
// Two glyph bits -> one chunky byte of non-zero (set) / zero pixels.
|
|
static const uint8_t kMonoPair[4] = { 0x00u, 0x0Fu, 0xF0u, 0xFFu };
|
|
const uint8_t *bits = sim->charset[chr];
|
|
uint8_t slot = (uint8_t)(chr & (ST_GLYPH_CACHE - 1u));
|
|
jlTileT *tile = &gRender.glyphTiles[slot];
|
|
uint8_t chunky[TILE_BYTES];
|
|
uint8_t row;
|
|
|
|
if (gRender.glyphUsed[slot] && memcmp(gRender.glyphBits[slot], bits, 8u) == 0) {
|
|
return tile;
|
|
}
|
|
for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) {
|
|
uint8_t b = bits[row];
|
|
uint8_t *p = &chunky[row * TILE_BYTES_PER_ROW];
|
|
p[0] = kMonoPair[b >> 6];
|
|
p[1] = kMonoPair[(b >> 4) & 3u];
|
|
p[2] = kMonoPair[(b >> 2) & 3u];
|
|
p[3] = kMonoPair[b & 3u];
|
|
}
|
|
tileFromChunky(tile, chunky);
|
|
memcpy(gRender.glyphBits[slot], bits, 8u);
|
|
gRender.glyphUsed[slot] = true;
|
|
return tile;
|
|
}
|
|
|
|
|
|
// Repaint the dirty cells: the list, or everything after an overflow
|
|
// or a scene change (which also restarts the per-character cell ranges).
|
|
static void paintCells(jlSurfaceT *stage, StSimT *sim) {
|
|
uint16_t cell;
|
|
uint16_t k;
|
|
uint8_t bx;
|
|
uint8_t by;
|
|
|
|
if (sim->dirtyAll) {
|
|
memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst));
|
|
memset(gRender.glyphLast, 0, sizeof(gRender.glyphLast));
|
|
cell = 0u;
|
|
for (by = 0u; by < ST_SCREEN_ROWS; by++) {
|
|
for (bx = 0u; bx < ST_SCREEN_COLS; bx++) {
|
|
pasteCell(stage, sim, cell, bx, by);
|
|
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];
|
|
by = cellRow(cell);
|
|
bx = (uint8_t)(cell - ((uint16_t)by << 5) - ((uint16_t)by << 3));
|
|
pasteCell(stage, sim, cell, bx, by);
|
|
sim->cellDirty[cell] = 0u;
|
|
}
|
|
sim->dirtyCount = 0u;
|
|
}
|
|
|
|
|
|
// Paint one cell: its glyph tile coloured from colour RAM over the
|
|
// background, and note the cell in the character's range.
|
|
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) {
|
|
uint8_t chr = sim->screen[cell];
|
|
|
|
if (cell < gRender.glyphFirst[chr]) {
|
|
gRender.glyphFirst[chr] = cell;
|
|
}
|
|
if (cell > gRender.glyphLast[chr]) {
|
|
gRender.glyphLast[chr] = cell;
|
|
}
|
|
jlTilePasteMono(stage, bx, by, glyphTile(sim, chr), sim->color[cell], sim->bgColor);
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// A chunky 8x8 tile (four bytes per row, high nibble = left pixel) as
|
|
// this port's jlTileT: a straight copy on the chunky ports, drawn and
|
|
// snapped through the scratch surface on the planar ones.
|
|
static void tileFromChunky(jlTileT *out, const uint8_t *chunky) {
|
|
#if defined(JOEYLIB_NATIVE_CHUNKY)
|
|
memcpy(out->pixels, chunky, TILE_BYTES);
|
|
#else
|
|
uint8_t row;
|
|
uint8_t k;
|
|
|
|
for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) {
|
|
for (k = 0u; k < TILE_BYTES_PER_ROW; k++) {
|
|
uint8_t b = chunky[row * TILE_BYTES_PER_ROW + k];
|
|
jlDrawPixel(gRender.scratch, (int16_t)(k * 2u), (int16_t)row, (uint8_t)(b >> 4));
|
|
jlDrawPixel(gRender.scratch, (int16_t)(k * 2u + 1u), (int16_t)row, (uint8_t)(b & 0x0Fu));
|
|
}
|
|
}
|
|
jlTileSnap(gRender.scratch, 0u, 0u, out);
|
|
#endif
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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));
|
|
memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst));
|
|
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.
|
|
// Load the offline-baked, already-compiled cel bank into the pinned
|
|
// cache so no cel is JIT-compiled at boot (each 65816 compile costs
|
|
// ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly
|
|
// as the play lookups: system ptr (level 0xFF), the level-wide colours.
|
|
// Returns false if the .spc is missing or built for another target/shift
|
|
// count (other ports, or a stale bake), so the caller falls back to the
|
|
// runtime JIT prewarm below. Its cross-platform contract makes this a
|
|
// no-op everywhere but the IIgs, where the bake is wired.
|
|
static bool loadPrecompiledCels(void) {
|
|
jlSpriteT *cels[64];
|
|
uint16_t n;
|
|
uint16_t i;
|
|
|
|
if (kStCelCount > (uint16_t)(sizeof(cels) / sizeof(cels[0])) || kStCelCount > ST_SPRITE_CACHE) {
|
|
return false;
|
|
}
|
|
n = jlSpriteBankLoadPrecompiled("sprites/staxicels.spc", cels, kStCelCount, 0);
|
|
if (n < kStCelCount) {
|
|
for (i = 0u; i < n; i++) {
|
|
jlSpriteDestroy(cels[i]);
|
|
}
|
|
return false;
|
|
}
|
|
for (i = 0u; i < n; i++) {
|
|
StSpriteCacheT *e = &gRender.cache[i];
|
|
e->sprite = cels[i];
|
|
e->ptr = kStCels[i].ptr;
|
|
e->color = kStCels[i].color;
|
|
e->multi = kStCels[i].multi;
|
|
e->mc0 = kStCels[i].mc0;
|
|
e->mc1 = kStCels[i].mc1;
|
|
e->level = 0xFFu;
|
|
e->used = true;
|
|
e->pinned = true;
|
|
e->lastUse = 0u;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
// Precompiled bank: instant, no per-cel JIT. Falls through to the
|
|
// runtime build below on any port without a matching .spc.
|
|
if (loadPrecompiledCels()) {
|
|
return;
|
|
}
|
|
saved = sim->frame;
|
|
gPrewarming = true;
|
|
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. Build it as a moving
|
|
// (idx <= 2) cel so it compiles: the level-name intro draws seven of
|
|
// them every frame, and the interpreter cannot keep up.
|
|
sim->frame.multiMask = 0u;
|
|
sim->frame.ptr[2] = 0xDAu;
|
|
sim->frame.color[2] = 0x07u;
|
|
(void)cachedSprite(sim, 2u);
|
|
sim->frame = saved;
|
|
gPrewarming = false;
|
|
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;
|
|
}
|
|
}
|