1115 lines
43 KiB
C
1115 lines
43 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 cell goes to the stage in one
|
|
// call: jlTilePasteGlyph takes the character's eight 1bpp charset rows
|
|
// as they stand and colours them from the cell's colour RAM, so an
|
|
// edited glyph needs no rebuild and a colour change costs nothing.
|
|
// 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.
|
|
//
|
|
// The title screen gets a special path (stRenderTitleLogo): its logo is
|
|
// 103 cells of one character that the flip-book animates by copying one
|
|
// of four frame glyphs over it, and a colour cycle that used to rewrite
|
|
// the colour of 407 cells. Both were pure repaint cost. Instead the four
|
|
// frames collapse into ONE tile whose pixels are drawn in reserved
|
|
// palette slots -- one slot per set of frames a pixel belongs to -- so a
|
|
// flip and a recolour are both a single palette write and no cell is
|
|
// ever repainted. The title's decoration sprites, which cycle colour
|
|
// with the logo, share one more reserved slot for the same reason.
|
|
|
|
#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.
|
|
// Room for the baked cab/passenger/exhaust set (37) plus the largest
|
|
// level's hook cels (I: 30) at once.
|
|
#define ST_SPRITE_CACHE 80u
|
|
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES)
|
|
#define ST_SPRITE_PX (8 * ST_SPRITE_TILES)
|
|
// Palette slots the title logo animation owns. The title screen draws
|
|
// in C64 colours 0, 1, 2, 6, 7, 11 and 12 only (plus colour 5 for the
|
|
// "GET READY" banner that appears over it), so these are free while it
|
|
// is up and the whole trick needs no per-scanline SCB band.
|
|
#define ST_PAL_LOGO_SPR 3u // the decoration sprites' cycling colour
|
|
#define ST_LOGO_PAL_SLOTS 7u
|
|
// The first hardware sprite that cycles colour with the logo.
|
|
#define ST_LOGO_SPR_FIRST 3u
|
|
|
|
// Where the startup message sits, and its C64 colour (1 = white).
|
|
#define ST_LOADING_ROW 12u
|
|
#define ST_LOADING_COLOR 1u
|
|
|
|
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)
|
|
bool compiled; // jlSpriteCompile succeeded for this cel
|
|
uint32_t lastUse; // for least-recently-used eviction
|
|
uint32_t retryAt; // cacheStamp at which a failed compile may retry
|
|
} 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 *stage;
|
|
jlSurfaceT *scratch;
|
|
// Title logo (stRenderTitleLogo): all four flip-book frames in one
|
|
// tile, animated by rewriting logoSlotMask[]'s palette slots.
|
|
bool logoMode;
|
|
bool starMode; // level intro: cycling the starfield
|
|
uint8_t starPhase; // which ring the bright band is on
|
|
uint8_t starTick; // frames until the band steps outward
|
|
bool logoTileOk; // the live glyph is one of the frames
|
|
uint8_t logoSlotCount;
|
|
uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in
|
|
uint8_t logoFrame; // frame the palette currently shows
|
|
uint8_t logoColor; // logo colour the palette currently shows
|
|
uint8_t logoSprColor; // decoration-sprite colour it shows
|
|
jlTileT logoTile;
|
|
// Cell range (inclusive) each character has been painted in since
|
|
// the last full repaint: where a changed glyph's cells can be.
|
|
// Column band of the cells about to be repainted, per character row
|
|
// (min > max = the row is clean), the same shape as the library's
|
|
// own per-row dirty bands.
|
|
uint8_t dirtyColMin[ST_SCREEN_ROWS];
|
|
uint8_t dirtyColMax[ST_SCREEN_ROWS];
|
|
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];
|
|
// ST_HW_SPRITES backups of ST_SPRITE_BACKUP_BYTES each, from jlAlloc:
|
|
// 4.5 KB that as BSS would not fit the IIgs entry bank (text, rodata,
|
|
// BSS and the C heap share its 64 KB).
|
|
uint8_t *backupMem;
|
|
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
|
|
};
|
|
|
|
// Palette slots the logo tile's pixels are drawn in, one per distinct
|
|
// set of flip-book frames a pixel belongs to. The four frames nest, so
|
|
// four of these are used; the rest are headroom if the frames change.
|
|
static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u };
|
|
|
|
// Level-intro warp: how a star ring looks at each distance BEHIND the
|
|
// bright band, as C64 colour indices (white, light grey, grey, dark
|
|
// grey, then black). A ring this far behind the band or further is
|
|
// unlit, so a lit head with a short fading tail sweeps outward and the
|
|
// rest of the field stays dark.
|
|
static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u };
|
|
#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0]))
|
|
// Frames per ring step. The intro runs at the game's 30 Hz tick, so a
|
|
// step every other frame sweeps the eight rings in about half a second.
|
|
#define ST_STAR_STEP_FRAMES 2u
|
|
|
|
|
|
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 void cacheCompile(StSpriteCacheT *e);
|
|
static StSpriteCacheT *cacheFreeSlot(void);
|
|
static void levelCelsEvict(void);
|
|
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
|
|
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
|
|
static uint8_t cellCol(uint16_t cell, uint8_t row);
|
|
static uint8_t cellRow(uint16_t cell);
|
|
static void collectGlyphs(StSimT *sim);
|
|
static void dirtyBands(const StSimT *sim);
|
|
static bool dirtyUnderSprite(int16_t px, int16_t py);
|
|
static uint8_t drawColor(const StSimT *sim, uint8_t idx);
|
|
static void dropCache(void);
|
|
static bool loadPrecompiledCels(void);
|
|
static bool logoBuildTile(const StSimT *sim);
|
|
static uint8_t logoFrameIndex(const StSimT *sim);
|
|
static void logoPaletteApply(const StSimT *sim, uint8_t frame);
|
|
static void starPaletteApply(void);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Compile a cached cel if it is not already, backing off after a failure
|
|
// (arena full) so a cel that cannot fit is not re-staged every frame --
|
|
// jlSpriteCompile's failure path runs the whole emitter twice.
|
|
#define ST_COMPILE_RETRY_STAMPS 4096u // ~8 frames' worth of cachedSprite calls per frame -> ~25 s
|
|
static void cacheCompile(StSpriteCacheT *e) {
|
|
if (e->compiled || gRender.cacheStamp < e->retryAt) {
|
|
return;
|
|
}
|
|
e->compiled = jlSpriteCompile(e->sprite);
|
|
if (!e->compiled) {
|
|
e->retryAt = gRender.cacheStamp + ST_COMPILE_RETRY_STAMPS;
|
|
}
|
|
}
|
|
|
|
|
|
// 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 = drawColor(sim, idx);
|
|
uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
|
|
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;
|
|
}
|
|
// No bitmap for this pointer (hooks enable a sprite a tick before
|
|
// they aim it, leaving it at block 0): nothing to draw, and no cel
|
|
// to build and evict for.
|
|
if (stSimSpriteBitmap(sim, ptr) == 0) {
|
|
return 0;
|
|
}
|
|
if (slot->used) {
|
|
jlSpriteDestroy(slot->sprite);
|
|
slot->sprite = 0;
|
|
slot->used = false;
|
|
slot->compiled = 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;
|
|
}
|
|
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a
|
|
// visible hitch. The cab, exhaust and passenger cels come baked
|
|
// (stRenderPrewarm), a level's hook cels come baked or are compiled
|
|
// at level entry (stRenderLevelCels), and a cel that still gets
|
|
// built here draws interpreted -- and is logged, because it means
|
|
// the level's cel list (stLevelCels.c) missed one.
|
|
slot->compiled = false;
|
|
slot->retryAt = 0u;
|
|
if (idx <= 2u && gPrewarming) {
|
|
cacheCompile(slot);
|
|
}
|
|
if (!gPrewarming) {
|
|
ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level);
|
|
}
|
|
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: cell / 40, read from the 125 distinct
|
|
// results of (cell >> 3) / 5. The old (x * 205) >> 10 identity avoided a
|
|
// division but still called the 65816's software multiply, and this runs
|
|
// twice for every repainted cell.
|
|
static const uint8_t kCellRow[125] = {
|
|
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4,
|
|
4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9,
|
|
9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12,
|
|
12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16,
|
|
16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 19,
|
|
19, 19, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22,
|
|
23, 23, 23, 23, 23, 24, 24, 24, 24, 24
|
|
};
|
|
|
|
|
|
// Column of a cell whose row is known: cell - row * 40 through a table,
|
|
// because the two-shift form of the multiply came back as a __mulhi3
|
|
// library call on the 65816.
|
|
static uint8_t cellCol(uint16_t cell, uint8_t row) {
|
|
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
|
|
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
|
|
520, 560, 600, 640, 680, 720, 760, 800, 840, 880, 920, 960
|
|
};
|
|
return (uint8_t)(cell - kRowBase[row]);
|
|
}
|
|
|
|
|
|
|
|
static uint8_t cellRow(uint16_t cell) {
|
|
return kCellRow[cell >> 3];
|
|
}
|
|
|
|
|
|
// A changed glyph (the hatch, the logo flip, the laser beams) makes
|
|
// every cell that shows it dirty. The simulation hands over a short list
|
|
// of the characters it edited, so nothing scans the charset; the cell
|
|
// search covers only the range the character was last painted in. The
|
|
// glyph itself needs no invalidation -- the paste reads the live charset
|
|
// every time.
|
|
static void collectGlyphs(StSimT *sim) {
|
|
uint8_t k;
|
|
uint16_t cell;
|
|
uint16_t next;
|
|
|
|
if (sim->charDirtyAll) {
|
|
sim->charDirtyAll = false;
|
|
sim->charDirtyCount = 0u;
|
|
return;
|
|
}
|
|
for (k = 0u; k < sim->charDirtyCount; k++) {
|
|
uint8_t ch = sim->charDirtyList[k];
|
|
if (sim->dirtyAll) {
|
|
continue;
|
|
}
|
|
// Exactly the cells showing `ch`, straight off the reverse index
|
|
// the sim maintains (StSimT in stSim.h). No scan, no screen[]
|
|
// test: a cell is in this list if and only if it shows `ch`.
|
|
//
|
|
// This replaced a walk of a per-glyph [first, last] span. That
|
|
// span only ever widened -- pasteCell pushed it out and nothing
|
|
// pulled it back -- so a glyph that once appeared in transient
|
|
// text kept a screen-wide span for the rest of the level and was
|
|
// rescanned on every animation frame, at 24-bit far-pointer cost
|
|
// per cell. It measured 22% of the whole program on level H.
|
|
//
|
|
// `next` is read before the body because the overflow arm below
|
|
// stops the walk, and because keeping the read up here makes the
|
|
// loop independent of anything the body does to the cell.
|
|
cell = sim->glyphHead[ch];
|
|
while (cell != ST_GLYPH_CELL_NONE) {
|
|
next = sim->cellNext[cell];
|
|
if (sim->cellDirty[cell] == 0u) {
|
|
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;
|
|
}
|
|
}
|
|
cell = next;
|
|
}
|
|
}
|
|
sim->charDirtyCount = 0u;
|
|
}
|
|
|
|
|
|
// Note where the cells about to be repainted are, as one column band
|
|
// per character row. This used to be a single bounding box over every
|
|
// dirty cell, and that box was wildly too coarse: the status row's
|
|
// score and fare meters sit at opposite ends of the screen and one of
|
|
// them moves on nearly every tick, so the box spanned the picture and
|
|
// every sprite counted as sitting over repainted ground -- all eight
|
|
// were undrawn and redrawn on about a third of all frames while only
|
|
// the cab and its exhaust had actually moved. Per row the bands are
|
|
// tight, and the rows a sprite covers are the only ones it tests.
|
|
static void dirtyBands(const StSimT *sim) {
|
|
uint16_t k;
|
|
|
|
uint8_t row;
|
|
|
|
if (sim->dirtyAll) {
|
|
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
|
|
gRender.dirtyColMin[row] = 0u;
|
|
gRender.dirtyColMax[row] = ST_SCREEN_COLS - 1u;
|
|
}
|
|
return;
|
|
}
|
|
// A plain loop, not memset: 25 entries do not pay for the 65816
|
|
// libc's far-called byte loop.
|
|
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
|
|
gRender.dirtyColMin[row] = 0xFFu;
|
|
gRender.dirtyColMax[row] = 0u;
|
|
}
|
|
for (k = 0u; k < sim->dirtyCount; k++) {
|
|
uint16_t cell = sim->dirtyList[k];
|
|
uint8_t r = cellRow(cell);
|
|
uint8_t col = cellCol(cell, r);
|
|
if (col < gRender.dirtyColMin[r]) {
|
|
gRender.dirtyColMin[r] = col;
|
|
}
|
|
if (col > gRender.dirtyColMax[r]) {
|
|
gRender.dirtyColMax[r] = col;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// True when a cell about to be repainted lies under the sprite drawn at
|
|
// (px, py): its save-under backup would hold stale background, so it
|
|
// has to be undrawn before the repaint and drawn again after.
|
|
static bool dirtyUnderSprite(int16_t px, int16_t py) {
|
|
int16_t x1 = (int16_t)(px + ST_SPRITE_W);
|
|
int16_t y1 = (int16_t)(py + ST_SPRITE_PX);
|
|
uint8_t row;
|
|
uint8_t rowEnd;
|
|
uint8_t colFirst;
|
|
uint8_t colLast;
|
|
|
|
if (x1 > SURFACE_WIDTH) {
|
|
x1 = SURFACE_WIDTH;
|
|
}
|
|
if (y1 > SURFACE_HEIGHT) {
|
|
y1 = SURFACE_HEIGHT;
|
|
}
|
|
// spriteOnScreen has already rejected anything fully off the stage,
|
|
// so only the top/left overhang needs clamping to row/column 0.
|
|
row = (uint8_t)(py < 0 ? 0 : (py >> 3));
|
|
rowEnd = (uint8_t)((y1 - 1) >> 3);
|
|
colFirst = (uint8_t)(px < 0 ? 0 : (px >> 3));
|
|
colLast = (uint8_t)((x1 - 1) >> 3);
|
|
for (; row <= rowEnd; row++) {
|
|
if (gRender.dirtyColMin[row] <= colLast && gRender.dirtyColMax[row] >= colFirst) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// The palette index a hardware sprite is drawn in. On the title screen
|
|
// sprites 3..7 all follow the logo's colour cycle, so they share one
|
|
// reserved slot: the cycle then recolours them with a palette write
|
|
// instead of rebuilding five cels, and -- because their drawn state
|
|
// stops changing -- without redrawing the sprites stacked above them.
|
|
static uint8_t drawColor(const StSimT *sim, uint8_t idx) {
|
|
if (gRender.logoMode && idx >= ST_LOGO_SPR_FIRST) {
|
|
return ST_PAL_LOGO_SPR;
|
|
}
|
|
return sim->frame.color[idx];
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Collapse the four flip-book frame glyphs into one tile: every pixel
|
|
// is drawn in the reserved palette slot that stands for the set of
|
|
// frames it is set in (no frame = colour 0, the background). Animating
|
|
// the logo is then a palette write per flip instead of a repaint of
|
|
// every cell showing it. Returns false if the frames need more distinct
|
|
// sets than there are reserved slots, leaving the ordinary per-cell
|
|
// repaint to draw the flip book.
|
|
static bool logoBuildTile(const StSimT *sim) {
|
|
uint8_t slotOfMask[1u << ST_LOGO_FRAMES];
|
|
uint8_t chunky[TILE_BYTES];
|
|
uint8_t row;
|
|
uint8_t col;
|
|
|
|
memset(slotOfMask, 0, sizeof(slotOfMask));
|
|
gRender.logoSlotCount = 0u;
|
|
for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) {
|
|
uint8_t *out = &chunky[row * TILE_BYTES_PER_ROW];
|
|
for (col = 0u; col < TILE_PIXELS_PER_SIDE; col++) {
|
|
uint8_t mask = 0u;
|
|
uint8_t f;
|
|
uint8_t slot;
|
|
for (f = 0u; f < ST_LOGO_FRAMES; f++) {
|
|
if (((stSimGlyphRow(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f), row) >> (7u - col)) & 1u) != 0u) {
|
|
mask |= (uint8_t)(1u << f);
|
|
}
|
|
}
|
|
if (mask != 0u && slotOfMask[mask] == 0u) {
|
|
if (gRender.logoSlotCount == ST_LOGO_PAL_SLOTS) {
|
|
return false;
|
|
}
|
|
slotOfMask[mask] = kLogoPalSlot[gRender.logoSlotCount];
|
|
gRender.logoSlotMask[gRender.logoSlotCount] = mask;
|
|
gRender.logoSlotCount++;
|
|
}
|
|
slot = slotOfMask[mask];
|
|
if ((col & 1u) == 0u) {
|
|
out[col >> 1] = (uint8_t)(slot << 4);
|
|
} else {
|
|
out[col >> 1] |= slot;
|
|
}
|
|
}
|
|
}
|
|
tileFromChunky(&gRender.logoTile, chunky);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Which flip-book frame the live logo glyph holds, or ST_LOGO_FRAMES if
|
|
// it matches none of them (then the logo tile does not stand for what
|
|
// the simulation is showing and the cells must be repainted normally).
|
|
static uint8_t logoFrameIndex(const StSimT *sim) {
|
|
uint8_t f;
|
|
|
|
for (f = 0u; f < ST_LOGO_FRAMES; f++) {
|
|
if (memcmp(stSimGlyph(sim, ST_LOGO_CHAR),
|
|
stSimGlyph(sim, (uint8_t)(ST_LOGO_FRAME_FIRST + f)), 8u) == 0) {
|
|
return f;
|
|
}
|
|
}
|
|
return ST_LOGO_FRAMES;
|
|
}
|
|
|
|
|
|
// Show flip-book frame `frame` in the logo's current colour: every
|
|
// reserved slot lights up in the colour if that frame is in its set and
|
|
// goes to the background colour if it is not. The decoration sprites'
|
|
// slot follows their own colour, which the cycle sets one step behind
|
|
// the logo's (the title's sprite table overwrites it on entry).
|
|
static void logoPaletteApply(const StSimT *sim, uint8_t frame) {
|
|
uint16_t pal[SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t on = kC64Palette[sim->logoColor & 15u];
|
|
uint16_t off = kC64Palette[sim->bgColor & 15u];
|
|
uint8_t k;
|
|
|
|
jlPaletteGet(gRender.stage, 0u, pal);
|
|
for (k = 0u; k < gRender.logoSlotCount; k++) {
|
|
pal[kLogoPalSlot[k]] = (((gRender.logoSlotMask[k] >> frame) & 1u) != 0u) ? on : off;
|
|
}
|
|
pal[ST_PAL_LOGO_SPR] = kC64Palette[sim->frame.color[ST_LOGO_SPR_FIRST] & 15u];
|
|
jlPaletteSet(gRender.stage, 0u, pal);
|
|
gRender.logoFrame = frame;
|
|
gRender.logoColor = sim->logoColor;
|
|
gRender.logoSprColor = sim->frame.color[ST_LOGO_SPR_FIRST];
|
|
}
|
|
|
|
|
|
// 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) {
|
|
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 = cellCol(cell, by);
|
|
pasteCell(stage, sim, cell, bx, by);
|
|
sim->cellDirty[cell] = 0u;
|
|
}
|
|
sim->dirtyCount = 0u;
|
|
}
|
|
|
|
|
|
// Paint one cell: the character's charset rows coloured from colour RAM
|
|
// over the background. Also notes the cell in the character's range,
|
|
// which is where a changed glyph's cells are looked for.
|
|
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) {
|
|
uint8_t chr = sim->screen[cell];
|
|
|
|
// On the title the logo's cells carry all four flip-book frames at
|
|
// once; the palette decides which one shows, so they are pasted as
|
|
// they are and never touched again.
|
|
if (gRender.logoTileOk && chr == ST_LOGO_CHAR) {
|
|
jlTilePaste(stage, bx, by, &gRender.logoTile);
|
|
return;
|
|
}
|
|
jlTilePasteGlyph(stage, bx, by, stSimGlyph(sim, chr), sim->color[cell], sim->bgColor);
|
|
}
|
|
|
|
|
|
// Light the starfield's ring colours for the current phase: the ring
|
|
// the band is on takes kStarRamp[0], the ones just inside it the rest
|
|
// of the ramp, and everything else the background. One palette write
|
|
// per step and not a single pixel moves -- that is the whole point of
|
|
// ST_STAR_RING_FIRST (stSim.h).
|
|
static void starPaletteApply(void) {
|
|
uint16_t pal[SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t color;
|
|
uint8_t behind;
|
|
uint8_t ring;
|
|
|
|
jlPaletteGet(gRender.stage, 0u, pal);
|
|
for (ring = 0u; ring < ST_STAR_RING_COUNT; ring++) {
|
|
// How far this ring sits behind the band, wrapping so the band
|
|
// re-enters at the centre as it leaves the edge.
|
|
behind = (uint8_t)((gRender.starPhase - ring) & (ST_STAR_RING_COUNT - 1u));
|
|
color = (behind < ST_STAR_RAMP_LEN) ? kC64Palette[kStarRamp[behind]] : kC64Palette[0];
|
|
pal[ST_STAR_RING_FIRST + ring] = color;
|
|
}
|
|
jlPaletteSet(gRender.stage, 0u, pal);
|
|
}
|
|
|
|
|
|
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) {
|
|
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.
|
|
if (gRender.logoMode) {
|
|
// The flip book only ever swaps the logo glyph for one of the
|
|
// four frames the logo tile already carries: recolour the
|
|
// reserved slots and drop the glyph's repaint on the floor.
|
|
uint8_t frame = logoFrameIndex(sim);
|
|
gRender.logoTileOk = (frame < ST_LOGO_FRAMES);
|
|
if (gRender.logoTileOk) {
|
|
// The flip book only swapped in a frame the logo tile already
|
|
// carries, so drop the repaint it asked for.
|
|
if (sim->charDirtyCount == 1u && sim->charDirtyList[0] == ST_LOGO_CHAR) {
|
|
sim->charDirtyCount = 0u;
|
|
}
|
|
if (frame != gRender.logoFrame || sim->logoColor != gRender.logoColor
|
|
|| sim->frame.color[ST_LOGO_SPR_FIRST] != gRender.logoSprColor) {
|
|
logoPaletteApply(sim, frame);
|
|
}
|
|
}
|
|
}
|
|
if (gRender.starMode) {
|
|
// The warp is the only thing animating here, and it animates in
|
|
// the palette: no cells change, no sprite moves for it, so the
|
|
// present that ends this frame copies no pixels at all.
|
|
gRender.starTick++;
|
|
if (gRender.starTick >= ST_STAR_STEP_FRAMES) {
|
|
gRender.starTick = 0u;
|
|
gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u));
|
|
starPaletteApply();
|
|
}
|
|
}
|
|
collectGlyphs(sim);
|
|
dirtyBands(sim);
|
|
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 & kStBit[idx]) != 0u ? 1u : 0u);
|
|
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 & kStBit[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 == drawColor(sim, idx) && d->multi == multi);
|
|
}
|
|
if (same && want && dirtyUnderSprite(px, py)) {
|
|
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 & kStBit[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 = drawColor(sim, idx);
|
|
d->multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u);
|
|
}
|
|
{
|
|
// 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 *backupMem = gRender.backupMem;
|
|
uint8_t k;
|
|
|
|
memset(&gRender, 0, sizeof(gRender));
|
|
if (backupMem == 0) {
|
|
backupMem = (uint8_t *)jlAlloc((uint32_t)ST_HW_SPRITES * ST_SPRITE_BACKUP_BYTES);
|
|
if (backupMem == 0) {
|
|
ST_LOG("stRenderInit: no memory for sprite backups");
|
|
}
|
|
}
|
|
gRender.backupMem = backupMem;
|
|
gRender.stage = stage;
|
|
for (k = 0u; k < ST_HW_SPRITES; k++) {
|
|
gRender.backup[k].bytes = (backupMem == 0) ? 0 : backupMem + (uint16_t)k * ST_SPRITE_BACKUP_BYTES;
|
|
}
|
|
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.
|
|
// The first unused cache slot, or NULL when the cache is full.
|
|
static StSpriteCacheT *cacheFreeSlot(void) {
|
|
uint8_t k;
|
|
|
|
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
|
if (!gRender.cache[k].used) {
|
|
return &gRender.cache[k];
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Drop every cel that belongs to a level (the baked set is level 0xFF and
|
|
// stays), freeing its arena code, and forget any hardware sprite's last
|
|
// hit on it.
|
|
static void levelCelsEvict(void) {
|
|
uint8_t k;
|
|
|
|
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
|
StSpriteCacheT *e = &gRender.cache[k];
|
|
uint8_t h;
|
|
if (!e->used || e->level == 0xFFu) {
|
|
continue;
|
|
}
|
|
for (h = 0u; h < ST_HW_SPRITES; h++) {
|
|
if (gRender.lastHit[h] == e) {
|
|
gRender.lastHit[h] = 0;
|
|
}
|
|
}
|
|
jlSpriteDestroy(e->sprite);
|
|
e->sprite = 0;
|
|
e->used = false;
|
|
e->pinned = false;
|
|
e->compiled = false;
|
|
}
|
|
}
|
|
|
|
|
|
// A level's own sprite block, or NULL when the level does not ship it.
|
|
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr) {
|
|
uint8_t k;
|
|
|
|
for (k = 0u; k < level->spriteCount; k++) {
|
|
if (level->sprites[k].ptr == ptr) {
|
|
return level->sprites[k].bitmap;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) {
|
|
jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u);
|
|
jlStagePresent();
|
|
}
|
|
|
|
|
|
// Ready a level's hook cels BEFORE the ride: the previous level's cels
|
|
// go, then the level's list (stLevelCelList) is filled from its baked
|
|
// bank (sprites/levelNN.spc) or, when there is none, built and compiled
|
|
// here behind the LOADING bar. Either way nothing compiles once the
|
|
// level is running. Keyed exactly as cachedSprite keys them, so every
|
|
// lookup during the ride is a hit.
|
|
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
|
static StCelDefT list[ST_LEVEL_CELS_MAX];
|
|
jlSpriteT *cels[ST_LEVEL_CELS_MAX];
|
|
char path[24];
|
|
uint8_t n;
|
|
uint8_t got;
|
|
uint8_t k;
|
|
|
|
levelCelsEvict();
|
|
if (level == 0) {
|
|
return;
|
|
}
|
|
n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX);
|
|
if (n == 0u) {
|
|
return;
|
|
}
|
|
memcpy(path, "sprites/level", 13u);
|
|
path[13] = (char)('0' + (level->levelIndex + 1u) / 10u);
|
|
path[14] = (char)('0' + (level->levelIndex + 1u) % 10u);
|
|
memcpy(path + 15, ".spc", 5u);
|
|
got = (uint8_t)jlSpriteBankLoadPrecompiled(path, cels, n, 0);
|
|
if (got == n) {
|
|
ST_LOG("spacetaxi: level %u cels from %s (%u)", level->levelIndex, path, n);
|
|
} else {
|
|
for (k = 0u; k < got; k++) {
|
|
jlSpriteDestroy(cels[k]);
|
|
}
|
|
ST_LOG("spacetaxi: level %u: no usable %s -- compiling %u cels at entry", level->levelIndex, path, n);
|
|
stRenderLoading(stage);
|
|
}
|
|
gPrewarming = true;
|
|
for (k = 0u; k < n; k++) {
|
|
StSpriteCacheT *slot = cacheFreeSlot();
|
|
if (slot == 0) {
|
|
ST_LOG("spacetaxi: ! sprite cache full at level cel %u", k);
|
|
break;
|
|
}
|
|
if (got == n) {
|
|
slot->sprite = cels[k];
|
|
slot->compiled = true;
|
|
} else {
|
|
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
|
|
if (bm == 0) {
|
|
continue;
|
|
}
|
|
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1);
|
|
slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES);
|
|
if (slot->sprite == 0) {
|
|
continue;
|
|
}
|
|
slot->compiled = false;
|
|
slot->retryAt = 0u;
|
|
cacheCompile(slot);
|
|
loadingBar(stage, (uint8_t)(k + 1u), n);
|
|
}
|
|
slot->ptr = list[k].ptr;
|
|
slot->color = list[k].color;
|
|
slot->multi = list[k].multi;
|
|
slot->mc0 = list[k].mc0;
|
|
slot->mc1 = list[k].mc1;
|
|
slot->level = level->levelIndex;
|
|
slot->used = true;
|
|
slot->pinned = true;
|
|
slot->lastUse = gRender.cacheStamp;
|
|
}
|
|
gPrewarming = false;
|
|
if (got != n) {
|
|
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
|
|
}
|
|
}
|
|
|
|
|
|
// Returns false if the .spc is missing or built for another target/shift
|
|
// count (a stale bake), so the caller falls back to the runtime JIT
|
|
// prewarm below. Every port stages its own bake (make/<port>.mk).
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
// "LOADING..." centred on an otherwise black stage. The charset is the
|
|
// game's own, indexed by ASCII (the C64 font's letters sit at their
|
|
// ASCII codes), so this needs no font surface and no simulation state.
|
|
void stRenderLoading(jlSurfaceT *stage) {
|
|
static const char kLoading[] = "LOADING...";
|
|
const uint8_t *charset = stC64Charset();
|
|
uint8_t col = (uint8_t)((ST_SCREEN_COLS - (sizeof(kLoading) - 1u)) / 2u);
|
|
uint8_t k;
|
|
|
|
jlSurfaceClear(stage, 0u);
|
|
for (k = 0u; k < (uint8_t)(sizeof(kLoading) - 1u); k++) {
|
|
jlTilePasteGlyph(stage, (uint8_t)(col + k), ST_LOADING_ROW,
|
|
&charset[(uint8_t)kLoading[k] * 8u], ST_LOADING_COLOR, 0u);
|
|
}
|
|
jlStagePresent();
|
|
}
|
|
|
|
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()) {
|
|
ST_LOG("spacetaxi: cels from sprites/staxicels.spc");
|
|
return;
|
|
}
|
|
ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm");
|
|
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++;
|
|
loadingBar(stage, done, total);
|
|
}
|
|
for (k = 0u; k < sizeof(kPassPtrs); k++) {
|
|
sim->frame.ptr[1] = kPassPtrs[k];
|
|
sim->frame.color[1] = 0x06u;
|
|
(void)cachedSprite(sim, 1u);
|
|
done++;
|
|
loadingBar(stage, done, total);
|
|
}
|
|
for (k = 0u; k < sizeof(kFlamePtrs); k++) {
|
|
sim->frame.ptr[2] = kFlamePtrs[k];
|
|
sim->frame.color[2] = 0x07u;
|
|
(void)cachedSprite(sim, 2u);
|
|
done++;
|
|
loadingBar(stage, done, total);
|
|
}
|
|
// 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;
|
|
|
|
if (gRender.logoMode || gRender.starMode) {
|
|
// The borrowed slots go back to being C64 colours.
|
|
gRender.logoMode = false;
|
|
gRender.logoTileOk = false;
|
|
gRender.starMode = false;
|
|
jlPaletteSet(gRender.stage, 0u, kC64Palette);
|
|
}
|
|
for (k = 0u; k < ST_HW_SPRITES; k++) {
|
|
gRender.slot[k].drawn = false;
|
|
}
|
|
stSimDirtyCells(sim);
|
|
sim->charDirtyAll = true;
|
|
sim->charDirtyCount = 0u;
|
|
}
|
|
|
|
|
|
// Turn on the title's palette-animated logo. Call it after
|
|
// stRenderSceneChanged (which turns it back off) on entry to the title
|
|
// screen; from then on a flip-book step and a colour cycle both cost
|
|
// one palette write and no cell repaint at all.
|
|
void stRenderTitleLogo(StSimT *sim) {
|
|
uint8_t frame;
|
|
|
|
if (!logoBuildTile(sim)) {
|
|
return;
|
|
}
|
|
frame = logoFrameIndex(sim);
|
|
gRender.logoMode = true;
|
|
gRender.logoTileOk = (frame < ST_LOGO_FRAMES);
|
|
logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u);
|
|
stSimDirtyCells(sim);
|
|
}
|
|
|
|
|
|
// Turn on the level intro's palette-cycled starfield. Call it after
|
|
// stRenderSceneChanged (which turns it back off) on entry to the level
|
|
// intro. The cells were painted by stIntroEnter; from here the warp is
|
|
// one palette write every ST_STAR_STEP_FRAMES frames.
|
|
void stRenderIntroStars(void) {
|
|
gRender.starMode = true;
|
|
gRender.starPhase = 0u;
|
|
gRender.starTick = 0u;
|
|
starPaletteApply();
|
|
}
|
|
|
|
|
|
void stRenderShutdown(void) {
|
|
dropCache();
|
|
if (gRender.scratch != 0) {
|
|
jlSurfaceDestroy(gRender.scratch);
|
|
gRender.scratch = 0;
|
|
}
|
|
jlFree(gRender.backupMem);
|
|
gRender.backupMem = 0;
|
|
}
|