More bug fixing and optimizations.

This commit is contained in:
Scott Duensing 2026-10-05 16:47:00 -05:00
parent efa9fecad9
commit a05958bd86
7 changed files with 338 additions and 178 deletions

View file

@ -4,8 +4,9 @@
# exact sprite-to-background collision bits), the standard sprite
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0)
# and the level-intro star velocity tables ($450C/$4513).
# 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0).
# (The level-intro star velocity tables, $450C/$4513, went with the moving
# warp; claudeDocs/spacetaxiSpriteWarp.patch puts them back.)
#
# Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE
# "bank ram" dump, 2-byte load-address header) so the port has one

View file

@ -27,6 +27,15 @@
#define ST_TICK_HZ 30u
#define ST_MAX_CATCHUP_TICKS 4u
// The C64 runs the game every other frame but the level intro ($4666)
// every frame.
#define ST_C64_FRAMES_PER_TICK 2u
// The intro's loop count ($4508) runs 0..3 and the cab's judder ($470D)
// steps -2 into count 0; batches that cannot show every frame end on the
// counts either side of that jump (runIntro).
#define ST_INTRO_LOOP_MASK 3u
#define ST_INTRO_SHOW_BEFORE 3u
#define ST_INTRO_SHOW_AFTER 0u
// Screens A..X, then screen Y -- the 25th, " MYSTERY SCREEN !!! " -- which
// $5167 loads once the 24-hour shifts have cleared all of A..X. Files are
// levels/level01.dat .. level25.dat, so Y is index 24.
@ -85,6 +94,8 @@ typedef struct {
bool fireArmed; // $445D: FIRE must be released first
uint8_t lastFrame;
uint16_t paceAcc;
uint16_t frameAcc; // C64 frames owed, in paceAcc's units
int8_t introOwed; // intro frames owed to (+) or run ahead of (-) the next batch
StStateE afterJingle; // where ST_STATE_JINGLE returns to
bool gameOverTune; // $5C24: song 5 started after the wait
uint8_t hsSlot; // $4D51 table slot being named
@ -189,11 +200,11 @@ static bool joyFire(void);
static uint8_t joyMask(void);
static bool loadLevel(uint8_t index);
static uint8_t nextLevelForShift(void);
static uint8_t paceTicks(void);
static uint8_t paceTicks(uint8_t *c64Frames);
static bool serviceJingle(void);
static void runCabbiesMenu(uint8_t ticks);
static void runDemoEnd(void);
static void runIntro(uint8_t ticks);
static void runIntro(uint8_t c64Frames);
static void runNameEntry(uint8_t ticks);
static bool nameEntryStart(void);
static void runPlaying(uint8_t ticks);
@ -604,22 +615,35 @@ static uint8_t nextLevelForShift(void) {
// Game ticks due this rendered frame, from the vertical blank count:
// one tick per two frames on 50/60 Hz displays, three per seven on
// the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
// length just clamps to the catch-up cap.
static uint8_t paceTicks(void) {
uint16_t hz = jlFrameHz();
uint8_t now = (uint8_t)jlFrameCount();
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
uint8_t ticks = 0u;
// length just clamps to the catch-up cap. *c64Frames gets the C64 frames
// due as well, ST_C64_FRAMES_PER_TICK a tick: one per display frame at
// 50/60 Hz, so the level intro shows every frame the C64 does -- the
// cab's up-and-down judder ($470D) is a one-frame pattern that stepping
// two frames at a time cancels out.
static uint8_t paceTicks(uint8_t *c64Frames) {
uint16_t hz = jlFrameHz();
uint8_t now = (uint8_t)jlFrameCount();
uint8_t frames = (uint8_t)(now - gGame.lastFrame);
uint8_t ticks = 0u;
uint16_t perTick = ST_TICK_HZ;
gGame.lastFrame = now;
if (frames > 16u) {
frames = 16u;
}
if (hz == 50u || hz == 60u) {
gGame.paceAcc += (uint16_t)(frames * 35u);
hz = 70u;
} else {
gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ);
perTick = 35u;
hz = 70u;
}
gGame.paceAcc += (uint16_t)(frames * perTick);
gGame.frameAcc += (uint16_t)(frames * perTick * ST_C64_FRAMES_PER_TICK);
*c64Frames = 0u;
while (gGame.frameAcc >= hz && *c64Frames < ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK) {
gGame.frameAcc = (uint16_t)(gGame.frameAcc - hz);
(*c64Frames)++;
}
if (gGame.frameAcc > (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK)) {
gGame.frameAcc = (uint16_t)(hz * ST_MAX_CATCHUP_TICKS * ST_C64_FRAMES_PER_TICK);
}
// Ticks not run this frame stay owed (a slow frame catches up over
// the next ones); a stall longer than the cap's worth is forgiven.
@ -674,15 +698,42 @@ static void runDemoEnd(void) {
}
// $4666 loop: two frames per game tick; joystick ends a demo.
static void runIntro(uint8_t ticks) {
// $4666 loop: once per C64 frame; joystick ends a demo. A port that shows
// every frame shows the cab's one-frame judder ($470D) as the C64 does. One
// that shows only some would sample it at a fixed phase -- every second frame
// shows -1, 0, -1, 0, a smooth climb -- so when two or more frames are due
// the batch is cut or stretched by one (evened out on the next batch) to end
// on the loop counts either side of the -2 jump: the frames shown then cross
// it and the cab judders, at the same average speed.
static void runIntro(uint8_t c64Frames) {
static const int8_t kTry[] = { 0, -1, 1 };
int16_t due;
uint8_t steps;
uint8_t end;
uint8_t t;
uint8_t k;
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
runDemoEnd();
return;
}
for (k = 0u; k < (uint8_t)(ticks * 2u); k++) {
due = (int16_t)(c64Frames + gGame.introOwed);
gGame.introOwed = 0;
steps = (uint8_t)((due > 0) ? due : 0);
if (c64Frames >= ST_C64_FRAMES_PER_TICK) {
for (t = 0u; t < (uint8_t)sizeof(kTry); t++) {
if (due + kTry[t] < 1) {
continue;
}
end = (uint8_t)((gSim.introLoopCount + due + kTry[t]) & ST_INTRO_LOOP_MASK);
if (end == ST_INTRO_SHOW_BEFORE || end == ST_INTRO_SHOW_AFTER) {
steps = (uint8_t)(due + kTry[t]);
gGame.introOwed = (int8_t)(-kTry[t]);
break;
}
}
}
for (k = 0u; k < steps; k++) {
if (stIntroStep(&gSim)) {
jlMusicStop(); // $4709
stSimEnterLevel(&gSim, &gLevel);
@ -930,7 +981,8 @@ static void startIntro(void) {
// $459C: the intro's tune plays under the fly-in, not blocking; the
// C64 leaves it to the IRQ player and stops it at $4709.
stAudioMusic(ST_SONG_INTRO, true);
gGame.state = ST_STATE_INTRO;
gGame.introOwed = 0;
gGame.state = ST_STATE_INTRO;
}
@ -1018,6 +1070,7 @@ int main(void) {
for (;;) {
uint8_t ticks;
uint8_t c64Frames;
if (gGame.fatal) {
fatalHold();
@ -1045,6 +1098,7 @@ int main(void) {
gGame.paused = false;
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
gGame.paceAcc = 0u;
gGame.frameAcc = 0u;
if (gGame.state == ST_STATE_INTRO) {
stAudioMusic(ST_SONG_INTRO, true); // $46B5
}
@ -1058,7 +1112,7 @@ int main(void) {
enterTitle();
}
}
ticks = paceTicks();
ticks = paceTicks(&c64Frames);
switch (gGame.state) {
case ST_STATE_TITLE:
runTitle(ticks);
@ -1090,7 +1144,7 @@ int main(void) {
}
break;
case ST_STATE_INTRO:
runIntro(ticks);
runIntro(c64Frames);
break;
case ST_STATE_PLAYING:
runPlaying(ticks);

View file

@ -323,8 +323,8 @@ static uint32_t speechFill(void *ctx, int8_t *dst, uint32_t count) {
}
n = (gSpeechStageCount < want) ? gSpeechStageCount : want;
if (n != 0u) {
// jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there),
// in two runs when the unread bytes wrap the ring's end.
// jlMemCopy (MVN on the IIgs), in two runs when the unread bytes
// wrap the ring's end.
uint8_t *to = (uint8_t *)dst;
uint16_t left = n;

View file

@ -2,8 +2,9 @@
// See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the
// cab set (flying, wreck and the intro decorations) and passenger set in
// multicolour at the constant level colours (cab/passenger 6, shared
// $D025/$D026 = 2/7), the flame set in multicolour colour 7, then the
// hires intro star in colour 7.
// $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No
// intro star: the level intro paints its starfield into the character
// screen (stTitle.c), so the star sprite is never drawn.
#include "stCels.h"
@ -12,7 +13,7 @@
#define MC0 0x02u
#define MC1 0x07u
#define CAB 0x06u // cab and passenger per-sprite colour
#define FLM 0x07u // flame / star colour
#define FLM 0x07u // flame colour
const StCelDefT kStCels[] = {
// Cab: flying (gear up/down x left/right), wreck cels, intro cabs.
@ -37,8 +38,6 @@ const StCelDefT kStCels[] = {
{ 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 },
{ 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 },
{ 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, FLM, 1u, MC0, MC1 },
// Intro star, hires colour 7.
{ 0xDA, FLM, 0u, 0u, 0u },
};
const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0]));

View file

@ -37,10 +37,10 @@
// 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
// cab, exhaust and passenger sets) 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 (36) 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)
@ -66,6 +66,10 @@ typedef struct {
uint8_t mc0;
uint8_t mc1;
uint8_t level; // level index the bitmap came from (level sprites)
uint8_t offX; // where the (cropped) cel sits in the 24x24
uint8_t offY; // VIC frame, in pixels -- see celCreate
uint8_t w; // the cel's size in pixels
uint8_t h;
bool used;
bool pinned; // prewarmed moving cel: never evicted (its
// compiled code is costly to re-emit on the 65816)
@ -78,6 +82,7 @@ typedef struct {
// What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
typedef struct {
bool drawn;
bool trusted; // backed up by the compiled, unclipped save
uint16_t x;
uint8_t y;
uint8_t ptr;
@ -92,8 +97,7 @@ typedef struct {
// 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
uint8_t starPhase; // which step of every flight is lit
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
@ -136,22 +140,18 @@ static const uint16_t kC64Palette[16] = {
// 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 };
// Level-intro warp: how a star step looks at each distance BEHIND the
// lit step, as C64 colour indices; further behind is unlit. One yellow
// star per ray, as the C64's star sprites (colour 7, $4523) fly with no
// trail.
static const uint8_t kStarRamp[] = { 7u };
#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 StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx);
static void cacheCompile(StSpriteCacheT *e);
static StSpriteCacheT *cacheFreeSlot(void);
static bool celCreate(StSpriteCacheT *e, const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1);
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);
@ -173,26 +173,7 @@ static bool prewarmReport(uint16_t expect);
static bool spriteOnScreen(int16_t px, int16_t py);
static void starPaletteApply(void);
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);
}
}
}
static bool tileHasPixels(const uint8_t *chunky);
// Compile a cached cel if it is not already, backing off after a failure
@ -210,9 +191,10 @@ static void cacheCompile(StSpriteCacheT *e) {
}
// 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) {
// The cached cel for hardware sprite `idx` of the current frame, built
// on first use from its VIC bitmap and colours; NULL when there is
// nothing to draw.
static StSpriteCacheT *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);
@ -227,14 +209,14 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
// 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;
return e;
}
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;
return e;
}
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the
// costly-to-recompile ones and are never evicted; the least-
@ -264,9 +246,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
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) {
if (!celCreate(slot, stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1)) {
return 0;
}
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a
@ -293,7 +273,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
slot->used = true;
slot->pinned = gPrewarming;
gRender.lastHit[idx] = slot;
return slot->sprite;
return slot;
}
@ -312,9 +292,61 @@ static const uint8_t kCellRow[125] = {
};
// 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.
// Build a VIC bitmap's cel into a cache entry from the shared cel
// expansion (stCels.c, the same one the offline baker uses), cropped to
// the tiles that hold pixels. Most VIC sprites leave whole tiles of their
// 24x21 frame empty, and every one of those would otherwise be saved,
// drawn, restored and presented each time the sprite moves. A bitmap
// with no pixels keeps the whole frame.
//
// The tiles go onto the scratch surface last to first: on the planar
// ports tileFromChunky converts through the scratch's top-left tile, so
// that tile has to be the one written last.
static bool celCreate(StSpriteCacheT *e, 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 txMin = ST_SPRITE_TILES;
uint8_t txMax = 0u;
uint8_t tyMin = ST_SPRITE_TILES;
uint8_t tyMax = 0u;
uint8_t tx;
uint8_t ty;
stCelBlob(bm, multi, color, mc0, mc1, blob);
ty = ST_SPRITE_TILES;
while (ty > 0u) {
ty--;
tx = ST_SPRITE_TILES;
while (tx > 0u) {
const uint8_t *chunk;
tx--;
chunk = &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES];
tileFromChunky(&tile, chunk);
jlTilePaste(gRender.scratch, tx, ty, &tile);
if (tileHasPixels(chunk)) {
txMin = (tx < txMin) ? tx : txMin;
txMax = (tx > txMax) ? tx : txMax;
tyMin = (ty < tyMin) ? ty : tyMin;
tyMax = (ty > tyMax) ? ty : tyMax;
}
}
}
if (txMin > txMax) {
txMin = 0u;
tyMin = 0u;
txMax = ST_SPRITE_TILES - 1u;
tyMax = ST_SPRITE_TILES - 1u;
}
e->offX = (uint8_t)(txMin * TILE_PIXELS_PER_SIDE);
e->offY = (uint8_t)(tyMin * TILE_PIXELS_PER_SIDE);
e->w = (uint8_t)((txMax - txMin + 1u) * TILE_PIXELS_PER_SIDE);
e->h = (uint8_t)((tyMax - tyMin + 1u) * TILE_PIXELS_PER_SIDE);
e->sprite = jlSpriteCreateFromSurface(gRender.scratch, (int16_t)e->offX, (int16_t)e->offY, (uint8_t)(e->w / TILE_PIXELS_PER_SIDE), (uint8_t)(e->h / TILE_PIXELS_PER_SIDE));
return e->sprite != 0;
}
// True for a cel that is MEANT to be built here rather than pre-listed,
// so the warning below stays about real misses. Only the title screen's
// decorations qualify: logoColorCycle recolours them every cycle step,
@ -335,6 +367,9 @@ static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) {
}
// 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,
@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) {
}
return;
}
// A plain loop, not memset: 25 entries do not pay for the 65816
// libc's far-called byte loop.
// One loop over both 25-entry arrays.
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
gRender.dirtyColMin[row] = 0xFFu;
gRender.dirtyColMax[row] = 0u;
@ -688,6 +722,19 @@ static void tileFromChunky(jlTileT *out, const uint8_t *chunky) {
}
// True when a chunky 8x8 tile has any non-transparent (non-zero) pixel.
static bool tileHasPixels(const uint8_t *chunky) {
uint8_t k;
for (k = 0u; k < TILE_BYTES; k++) {
if (chunky[k] != 0u) {
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
}
}
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));
// The warp animates in the palette: no cells change and no sprite
// moves for it. The lit step follows the intro's own frame count,
// so the stars keep the C64's speed whatever the frame rate.
uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
if (phase != gRender.starPhase) {
gRender.starPhase = phase;
starPaletteApply();
}
}
@ -752,17 +799,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = ST_HW_SPRITES; k > first; k--) {
StDrawnT *d = &gRender.slot[k - 1u];
if (d->drawn) {
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
if (d->trusted) {
jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]);
} else {
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;
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k];
StSpriteCacheT *e;
int16_t px;
int16_t py;
if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
continue;
}
@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
if (!spriteOnScreen(px, py)) {
continue;
}
sp = cachedSprite(sim, idx);
if (sp == 0) {
e = cachedSprite(sim, idx);
if (e == 0) {
continue;
}
jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]);
px = (int16_t)(px + e->offX);
py = (int16_t)(py + e->offY);
// A compiled cel wholly on the stage takes the trusted calls,
// which skip the per-call geometry checks; one that is clipped
// or interpreted needs the checked ones, and so does its restore.
d->trusted = e->compiled && px >= 0 && py >= 0 && px <= (int16_t)(SURFACE_WIDTH - e->w) && py <= (int16_t)(SURFACE_HEIGHT - e->h);
if (d->trusted) {
jlSpriteSaveAndDrawTrusted(stage, e->sprite, px, py, &gRender.backup[k]);
} else {
jlSpriteSaveAndDraw(stage, e->sprite, px, py, &gRender.backup[k]);
}
d->drawn = true;
d->x = sim->frame.x[idx];
d->y = sim->frame.y[idx];
@ -824,14 +885,6 @@ void stRenderInit(jlSurfaceT *stage) {
}
// 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;
@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
}
if (got == n) {
slot->sprite = cels[k];
slot->compiled = true;
slot->offX = 0u;
slot->offY = 0u;
slot->w = ST_SPRITE_PX;
slot->h = ST_SPRITE_PX;
slot->compiled = (jlSpriteCompiledSize(cels[k]) != 0u);
} 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) {
if (!celCreate(slot, bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1)) {
continue;
}
slot->compiled = false;
@ -975,6 +1030,10 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
}
// 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 (a stale bake), so the caller falls back to the runtime JIT
// prewarm below. Every port stages its own bake (make/<port>.mk).
@ -1002,6 +1061,10 @@ static bool loadPrecompiledCels(void) {
e->mc0 = kStCels[i].mc0;
e->mc1 = kStCels[i].mc1;
e->level = 0xFFu;
e->offX = 0u;
e->offY = 0u;
e->w = ST_SPRITE_PX;
e->h = ST_SPRITE_PX;
e->used = true;
e->pinned = true;
e->lastUse = 0u;
@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) {
jlStagePresent();
}
// Build (and compile) the cels the game draws every screen -- cab,
// exhaust, passenger, wreck and intro cab -- up front, so no frame pays
// for a sprite build mid-play. A bar on the stage shows progress on the
// slower ports.
bool 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 total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs));
uint8_t done = 0u;
bool ok;
uint8_t k;
@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
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;
ok = prewarmReport(total);
@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *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.
// one palette write every ST_STAR_FRAMES_PER_STEP intro frames.
void stRenderIntroStars(void) {
gRender.starMode = true;
gRender.starPhase = 0u;
gRender.starTick = 0u;
starPaletteApply();
}

28
stSim.h
View file

@ -100,8 +100,9 @@
// pixel rows.
//
// The contract between the two halves is just this colour range:
// stTitle.c colours a star's cell ST_STAR_RING_FIRST + ring (ring 0 is
// the centre), and the renderer cycles that range. These are C64 colour
// stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its
// ray's flight (offset by the ray's launch delay), and the renderer
// cycles that range. These are C64 colour
// indices the intro screen never uses -- it draws in 0, 3 and, in demo
// mode, 4 and 5; the cab sprite adds 2, 6 and 7.
#define ST_STAR_RING_FIRST 8u
@ -112,12 +113,14 @@
// something else must fail to compile here instead of quietly making
// the warp skip rings.
typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1];
// Ring 0 is the centre, which in a warp is the VANISHING POINT: those
// stars are the far ones and get the small glyph. The big glyph belongs
// to the outer rings, the stars about to rush past the viewer.
#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings
#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings
#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant
// A C64 star flies $20 frames ($4500) and the painted field marks every
// ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on
// that many intro frames (introStepCount): the warp keeps the C64's speed at
// any frame rate.
#define ST_INTRO_STAR_FLIGHT 0x20u
#define ST_STAR_FRAMES_PER_STEP (ST_INTRO_STAR_FLIGHT / ST_STAR_RING_COUNT)
// Every star is the small glyph: the C64's star is a 5x3 speck.
#define ST_STAR_GLYPH 0x2Eu // '.'
// VIC sprite block pointers the game selects by name. The cab's bit 0 is
// the landing gear, so each facing is a pair.
@ -309,7 +312,11 @@ typedef struct {
uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
} StLevelT;
// One VIC sprite as the game sees it through its shadow tables.
// One VIC sprite as the game sees it through its shadow tables. Padded
// to eight bytes: the 65816 compiler indexes a 6-byte element through
// a call to its 32-bit multiply routine (sim is a far pointer), and
// every sim->spr[idx] in the game paid for one; a power-of-two stride
// is three shifts.
typedef struct {
uint8_t col; // $7175,X X low byte
uint8_t msb; // $7185,X X high bit (any non-zero value = set)
@ -317,7 +324,9 @@ typedef struct {
uint8_t enable; // $718E,X
uint8_t ptr; // $7197,X block pointer
uint8_t color; // $719F,X
uint8_t strideToPow2[2];
} StSpriteT;
typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1];
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the
// renderer draws this and the collision test evaluates it.
@ -542,6 +551,7 @@ typedef struct {
uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6
uint8_t introLoopCount; // $4508
uint8_t introStepCount; // port: intro frames run, for the painted warp
uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B
uint8_t spriteMc0; // $D025

142
stTitle.c
View file

@ -14,8 +14,10 @@
//
// The seven star sprites are replaced by a static starfield whose
// colours are cycled -- see ST_STAR_RING_FIRST in stSim.h for why and
// how. The cab still flies, and it is still what ends the intro, so the
// sequence keeps its length and its cue; only the warp changes.
// how. The stars sit on the seven rays the C64's stars fly, where those
// stars are every few frames of a flight, so the field plays the
// original's paths. The cab still flies, and it is still what ends the
// intro, so the sequence keeps its length and its cue.
#include <string.h>
@ -40,42 +42,35 @@ static const uint8_t kTextScreens[] = "THIS IS 1 OF 25 DIFFERENT SCREENS!";
#define ST_TITLE_SPARKLE_TICKS 0x5Au
#define ST_TITLE_HOVER_COL 0x28u
#define ST_INTRO_STAR_PTR 0xDAu
#define ST_INTRO_STAR_COL 0xAAu // where a star launches ($4523)
#define ST_INTRO_STAR_ROW 0x8Cu
#define ST_INTRO_STAR_DELAYS 0x20u // launch delays drawn from 0..$1F ($44F8)
#define ST_INTRO_CAB_END_ROW 0x94u
#define ST_INTRO_CAB_DX_RELOAD 0x28u
// The palette-cycled starfield. Seven stars per ring so the bright band
// stays the same weight all the way out, placed at fixed pseudo-random
// angles and clear of the three lines the intro screen writes (the level
// name on row 12, and the demo hints on rows 3 and 24).
typedef struct {
uint8_t col;
uint8_t row;
uint8_t ring;
} StStarT;
// The palette-cycled starfield. The C64 flies one star sprite along each
// of seven rays at these velocities ($450C / $4513, pixels a frame) for
// $20 frames ($4500), then launches it again from the start. The painted
// field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP
// frames; the renderer lights one step of every ray at a time, so a star
// hops out along its ray. A star is drawn where the sprite's image ($DA:
// a 5x3 speck centred on pixel 11, 9 of the sprite) would be.
#define ST_INTRO_STARS 7u
#define ST_INTRO_STAR_IMAGE_X 11 // the speck's centre in its sprite
#define ST_INTRO_STAR_IMAGE_Y 9
#define ST_SCREEN_WIDTH_PX ((int16_t)(ST_SCREEN_COLS * 8u))
#define ST_SCREEN_HEIGHT_PX ((int16_t)(ST_SCREEN_ROWS * 8u))
#define ST_CELL_SHIFT 3 // 8 pixels a cell
static const StStarT kIntroStar[] = {
{ 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u },
{ 21u, 13u, 0u }, { 22u, 14u, 0u }, { 18u, 15u, 0u }, { 18u, 8u, 1u },
{ 19u, 8u, 1u }, { 14u, 10u, 1u }, { 25u, 14u, 1u }, { 21u, 15u, 1u },
{ 25u, 15u, 1u }, { 20u, 17u, 1u }, { 26u, 7u, 2u }, { 12u, 10u, 2u },
{ 29u, 11u, 2u }, { 11u, 13u, 2u }, { 29u, 14u, 2u }, { 21u, 17u, 2u },
{ 23u, 18u, 2u }, { 21u, 4u, 3u }, { 24u, 4u, 3u }, { 11u, 8u, 3u },
{ 12u, 8u, 3u }, { 10u, 10u, 3u }, { 30u, 16u, 3u }, { 14u, 17u, 3u },
{ 15u, 2u, 4u }, { 8u, 8u, 4u }, { 32u, 9u, 4u }, { 9u, 17u, 4u },
{ 32u, 17u, 4u }, { 32u, 18u, 4u }, { 24u, 22u, 4u }, { 16u, 1u, 5u },
{ 28u, 1u, 5u }, { 31u, 2u, 5u }, { 36u, 13u, 5u }, { 6u, 15u, 5u },
{ 4u, 16u, 5u }, { 5u, 19u, 5u }, { 8u, 0u, 6u }, { 30u, 0u, 6u },
{ 1u, 8u, 6u }, { 2u, 12u, 6u }, { 37u, 16u, 6u }, { 7u, 21u, 6u },
{ 32u, 22u, 6u }, { 0u, 4u, 7u }, { 39u, 7u, 7u }, { 0u, 10u, 7u },
{ 38u, 20u, 7u }, { 38u, 21u, 7u }, { 4u, 22u, 7u }, { 35u, 22u, 7u },
};
#define ST_INTRO_STAR_COUNT (sizeof(kIntroStar) / sizeof(kIntroStar[0]))
static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 };
static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 };
static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta);
static void logoColorCycle(StSimT *sim);
static void logoFlip(StSimT *sim);
static void paintStarfield(StSimT *sim);
static void paintStarfield(StSimT *sim, const uint8_t *delay);
static bool rowHasText(const StSimT *sim, uint8_t row);
// $4113
@ -124,21 +119,60 @@ static void logoFlip(StSimT *sim) {
}
// Paint the level-intro starfield into screen RAM, once. A star's cell
// is coloured ST_STAR_RING_FIRST + its ring and never touched again --
// the renderer animates the warp by cycling those colours, which costs
// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h.
static void paintStarfield(StSimT *sim) {
const StStarT *star;
uint8_t one[2];
uint8_t k;
// Paint the level-intro starfield into screen RAM, once. A star's cell is
// coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by
// its ray's launch delay so the rays fire out of step as on the C64, and is
// never touched again -- the renderer animates the warp by cycling those
// colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h).
// Step 0 is the launch point, which every ray shares, and steps off the
// screen are left out, as are rows that carry the intro's text: on the C64
// the stars fly over the text, but a cell star there reads as part of it --
// a gap in the level name, or a full stop after the demo hint.
static void paintStarfield(StSimT *sim, const uint8_t *delay) {
static const uint8_t one[2] = { ST_STAR_GLYPH, 0u }; // stSimDrawText stops below screen code 6
bool textRow[ST_SCREEN_ROWS];
int16_t originX;
int16_t originY;
int16_t x;
int16_t y;
uint8_t row;
uint8_t ray;
uint8_t step;
uint8_t slot;
one[1] = 0u; // stSimDrawText stops below screen code 6
for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) {
star = &kIntroStar[k];
one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR;
stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring));
// Before any star goes down, or the first star on a row would hide the
// rest of that row's.
for (row = 0u; row < ST_SCREEN_ROWS; row++) {
textRow[row] = rowHasText(sim, row);
}
originX = (int16_t)(ST_INTRO_STAR_COL - ST_SPRITE_X_ORIGIN + ST_INTRO_STAR_IMAGE_X);
originY = (int16_t)(ST_INTRO_STAR_ROW - ST_SPRITE_Y_ORIGIN + ST_INTRO_STAR_IMAGE_Y);
for (ray = 0u; ray < ST_INTRO_STARS; ray++) {
for (step = 1u; step < ST_STAR_RING_COUNT; step++) {
x = (int16_t)(originX + kIntroStarDx[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP));
y = (int16_t)(originY + kIntroStarDy[ray] * (int16_t)(step * ST_STAR_FRAMES_PER_STEP));
if (x < 0 || x >= ST_SCREEN_WIDTH_PX || y < 0 || y >= ST_SCREEN_HEIGHT_PX || textRow[y >> ST_CELL_SHIFT]) {
continue;
}
slot = (uint8_t)((step + delay[ray] / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
stSimDrawText(sim, (uint8_t)(x >> ST_CELL_SHIFT), (uint8_t)(y >> ST_CELL_SHIFT), one, (uint8_t)(ST_STAR_RING_FIRST + slot));
}
}
}
// True when screen row `row` holds anything but blanks.
static bool rowHasText(const StSimT *sim, uint8_t row) {
const uint8_t *cell;
uint8_t col;
cell = &sim->screen[(uint16_t)row * ST_SCREEN_COLS];
for (col = 0u; col < ST_SCREEN_COLS; col++) {
if (cell[col] != ST_CHAR_SPACE) {
return true;
}
}
return false;
}
@ -148,6 +182,7 @@ static void paintStarfield(StSimT *sim) {
// $4523..$4665 -- set up the level-name screen.
void stIntroEnter(StSimT *sim, const StLevelT *level) {
uint8_t delay[ST_INTRO_STARS];
uint8_t k;
sim->level = level;
@ -161,20 +196,19 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->spr[7].row = 0xE4u;
sim->introCabDxTimer = 0x14u;
sim->introCabDx = 0x01u;
for (k = 0u; k < 7u; k++) {
uint8_t idx = (uint8_t)(6u - k);
sim->spr[idx].row = 0x8Cu;
for (k = 0u; k < ST_INTRO_STARS; k++) {
uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k);
sim->spr[idx].row = ST_INTRO_STAR_ROW;
sim->spr[idx].msb = 0u;
sim->spr[idx].col = 0xAAu;
sim->spr[idx].col = ST_INTRO_STAR_COL;
sim->spr[idx].color = 0x07u;
sim->spr[idx].ptr = ST_INTRO_STAR_PTR;
sim->spr[idx].enable = 0u;
// $44F8/$4500 held each star's spawn timer and reload. Nothing
// reads them now that the warp is painted rather than flown, but
// the seven RNG draws MUST stay: they advance the shared RNG and
// the level that follows draws from it, so dropping them would
// change the game that comes after the intro.
(void)stSimRng(sim, 0x20u);
// $44F8: each star's launch delay. The painted field uses it to
// put the rays out of step, and the seven RNG draws MUST stay in
// any case: they advance the shared RNG and the level that
// follows draws from it.
delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS);
}
sim->borderColor = 0u;
sim->bgColor = 0u;
@ -188,12 +222,13 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->charDirtyAll = true;
sim->charDirtyCount = 0u;
stSimDrawText(sim, 10u, 12u, level->name, 3u);
paintStarfield(sim);
if (sim->demoMode != 0u && sim->postMortem == 0u) {
stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u);
stSimDrawText(sim, 3u, 24u, kTextScreens, 4u);
}
paintStarfield(sim, delay);
sim->introLoopCount = 0u;
sim->introStepCount = 0u;
}
@ -203,6 +238,7 @@ bool stIntroStep(StSimT *sim) {
uint8_t k;
stSimMarshal(sim);
sim->introStepCount++;
sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u);
addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]);
if ((sim->introLoopCount & 1u) == 0u) {