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 # exact sprite-to-background collision bits), the standard sprite
# bitmaps (cab, passenger, flame, warp, death, intro star, leaving # bitmaps (cab, passenger, flame, warp, death, intro star, leaving
# cels), the demo-mode RNG table ($446A), the SID SFX programs (the # cels), the demo-mode RNG table ($446A), the SID SFX programs (the
# 9-byte blocks fed to $42E9), the engine-flame pointer table ($6DB0) # 9-byte blocks fed to $42E9) and the engine-flame pointer table ($6DB0).
# and the level-intro star velocity tables ($450C/$4513). # (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 # Everything comes straight out of stuff/spacetaxi/raw.bin (a VICE
# "bank ram" dump, 2-byte load-address header) so the port has one # "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_TICK_HZ 30u
#define ST_MAX_CATCHUP_TICKS 4u #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 // 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 // $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. // levels/level01.dat .. level25.dat, so Y is index 24.
@ -85,6 +94,8 @@ typedef struct {
bool fireArmed; // $445D: FIRE must be released first bool fireArmed; // $445D: FIRE must be released first
uint8_t lastFrame; uint8_t lastFrame;
uint16_t paceAcc; 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 StStateE afterJingle; // where ST_STATE_JINGLE returns to
bool gameOverTune; // $5C24: song 5 started after the wait bool gameOverTune; // $5C24: song 5 started after the wait
uint8_t hsSlot; // $4D51 table slot being named uint8_t hsSlot; // $4D51 table slot being named
@ -189,11 +200,11 @@ static bool joyFire(void);
static uint8_t joyMask(void); static uint8_t joyMask(void);
static bool loadLevel(uint8_t index); static bool loadLevel(uint8_t index);
static uint8_t nextLevelForShift(void); static uint8_t nextLevelForShift(void);
static uint8_t paceTicks(void); static uint8_t paceTicks(uint8_t *c64Frames);
static bool serviceJingle(void); static bool serviceJingle(void);
static void runCabbiesMenu(uint8_t ticks); static void runCabbiesMenu(uint8_t ticks);
static void runDemoEnd(void); static void runDemoEnd(void);
static void runIntro(uint8_t ticks); static void runIntro(uint8_t c64Frames);
static void runNameEntry(uint8_t ticks); static void runNameEntry(uint8_t ticks);
static bool nameEntryStart(void); static bool nameEntryStart(void);
static void runPlaying(uint8_t ticks); 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: // 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 // 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 // the 70 Hz VGA mode. jlFrameCount is 8-bit-wrapped so a stall of any
// length just clamps to the catch-up cap. // length just clamps to the catch-up cap. *c64Frames gets the C64 frames
static uint8_t paceTicks(void) { // 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(); uint16_t hz = jlFrameHz();
uint8_t now = (uint8_t)jlFrameCount(); uint8_t now = (uint8_t)jlFrameCount();
uint8_t frames = (uint8_t)(now - gGame.lastFrame); uint8_t frames = (uint8_t)(now - gGame.lastFrame);
uint8_t ticks = 0u; uint8_t ticks = 0u;
uint16_t perTick = ST_TICK_HZ;
gGame.lastFrame = now; gGame.lastFrame = now;
if (frames > 16u) { if (frames > 16u) {
frames = 16u; frames = 16u;
} }
if (hz == 50u || hz == 60u) { if (hz == 50u || hz == 60u) {
gGame.paceAcc += (uint16_t)(frames * 35u); perTick = 35u;
hz = 70u; hz = 70u;
} else { }
gGame.paceAcc += (uint16_t)(frames * ST_TICK_HZ); 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 // 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. // 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. // $4666 loop: once per C64 frame; joystick ends a demo. A port that shows
static void runIntro(uint8_t ticks) { // 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; uint8_t k;
if (gGame.demo && (joyMask() & 0x1Fu) != 0u) { if (gGame.demo && (joyMask() & 0x1Fu) != 0u) {
runDemoEnd(); runDemoEnd();
return; 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)) { if (stIntroStep(&gSim)) {
jlMusicStop(); // $4709 jlMusicStop(); // $4709
stSimEnterLevel(&gSim, &gLevel); stSimEnterLevel(&gSim, &gLevel);
@ -930,6 +981,7 @@ static void startIntro(void) {
// $459C: the intro's tune plays under the fly-in, not blocking; the // $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. // C64 leaves it to the IRQ player and stops it at $4709.
stAudioMusic(ST_SONG_INTRO, true); stAudioMusic(ST_SONG_INTRO, true);
gGame.introOwed = 0;
gGame.state = ST_STATE_INTRO; gGame.state = ST_STATE_INTRO;
} }
@ -1018,6 +1070,7 @@ int main(void) {
for (;;) { for (;;) {
uint8_t ticks; uint8_t ticks;
uint8_t c64Frames;
if (gGame.fatal) { if (gGame.fatal) {
fatalHold(); fatalHold();
@ -1045,6 +1098,7 @@ int main(void) {
gGame.paused = false; gGame.paused = false;
gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed gGame.lastFrame = (uint8_t)jlFrameCount(); // the held time is not owed
gGame.paceAcc = 0u; gGame.paceAcc = 0u;
gGame.frameAcc = 0u;
if (gGame.state == ST_STATE_INTRO) { if (gGame.state == ST_STATE_INTRO) {
stAudioMusic(ST_SONG_INTRO, true); // $46B5 stAudioMusic(ST_SONG_INTRO, true); // $46B5
} }
@ -1058,7 +1112,7 @@ int main(void) {
enterTitle(); enterTitle();
} }
} }
ticks = paceTicks(); ticks = paceTicks(&c64Frames);
switch (gGame.state) { switch (gGame.state) {
case ST_STATE_TITLE: case ST_STATE_TITLE:
runTitle(ticks); runTitle(ticks);
@ -1090,7 +1144,7 @@ int main(void) {
} }
break; break;
case ST_STATE_INTRO: case ST_STATE_INTRO:
runIntro(ticks); runIntro(c64Frames);
break; break;
case ST_STATE_PLAYING: case ST_STATE_PLAYING:
runPlaying(ticks); 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; n = (gSpeechStageCount < want) ? gSpeechStageCount : want;
if (n != 0u) { if (n != 0u) {
// jlMemCopy (MVN on the IIgs; libc memcpy is a byte loop there), // jlMemCopy (MVN on the IIgs), in two runs when the unread bytes
// in two runs when the unread bytes wrap the ring's end. // wrap the ring's end.
uint8_t *to = (uint8_t *)dst; uint8_t *to = (uint8_t *)dst;
uint16_t left = n; uint16_t left = n;

View file

@ -2,8 +2,9 @@
// See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the // See stCels.h. Ordered exactly as stRenderPrewarm built the cels: the
// cab set (flying, wreck and the intro decorations) and passenger set in // cab set (flying, wreck and the intro decorations) and passenger set in
// multicolour at the constant level colours (cab/passenger 6, shared // multicolour at the constant level colours (cab/passenger 6, shared
// $D025/$D026 = 2/7), the flame set in multicolour colour 7, then the // $D025/$D026 = 2/7), then the flame set in multicolour colour 7. No
// hires intro star in colour 7. // intro star: the level intro paints its starfield into the character
// screen (stTitle.c), so the star sprite is never drawn.
#include "stCels.h" #include "stCels.h"
@ -12,7 +13,7 @@
#define MC0 0x02u #define MC0 0x02u
#define MC1 0x07u #define MC1 0x07u
#define CAB 0x06u // cab and passenger per-sprite colour #define CAB 0x06u // cab and passenger per-sprite colour
#define FLM 0x07u // flame / star colour #define FLM 0x07u // flame colour
const StCelDefT kStCels[] = { const StCelDefT kStCels[] = {
// Cab: flying (gear up/down x left/right), wreck cels, intro cabs. // 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 }, { 0xD5, FLM, 1u, MC0, MC1 }, { 0xD2, FLM, 1u, MC0, MC1 },
{ 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 }, { 0xD1, FLM, 1u, MC0, MC1 }, { 0xD7, FLM, 1u, MC0, MC1 },
{ 0xD3, FLM, 1u, MC0, MC1 }, { 0xD6, 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])); 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 // 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 // entry bank below the I/O window; the caches are sized down there. The
// sprite cache must still hold every cel stRenderPrewarm builds (the // sprite cache must still hold every cel stRenderPrewarm builds (the
// cab, exhaust and passenger sets plus the intro star) or a cel change // cab, exhaust and passenger sets) or a cel change mid-play rebuilds
// mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped // and recompiles a sprite. Glyphs are direct-mapped by character, so
// by character, so the count is a power of two. // the count is a power of two.
// Room for the baked cab/passenger/exhaust set (37) plus the largest // Room for the baked cab/passenger/exhaust set (36) plus the largest
// level's hook cels (I: 30) at once. // level's hook cels (I: 30) at once.
#define ST_SPRITE_CACHE 80u #define ST_SPRITE_CACHE 80u
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #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 mc0;
uint8_t mc1; uint8_t mc1;
uint8_t level; // level index the bitmap came from (level sprites) 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 used;
bool pinned; // prewarmed moving cel: never evicted (its bool pinned; // prewarmed moving cel: never evicted (its
// compiled code is costly to re-emit on the 65816) // 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). // What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0).
typedef struct { typedef struct {
bool drawn; bool drawn;
bool trusted; // backed up by the compiled, unclipped save
uint16_t x; uint16_t x;
uint8_t y; uint8_t y;
uint8_t ptr; uint8_t ptr;
@ -92,8 +97,7 @@ typedef struct {
// tile, animated by rewriting logoSlotMask[]'s palette slots. // tile, animated by rewriting logoSlotMask[]'s palette slots.
bool logoMode; bool logoMode;
bool starMode; // level intro: cycling the starfield bool starMode; // level intro: cycling the starfield
uint8_t starPhase; // which ring the bright band is on uint8_t starPhase; // which step of every flight is lit
uint8_t starTick; // frames until the band steps outward
bool logoTileOk; // the live glyph is one of the frames bool logoTileOk; // the live glyph is one of the frames
uint8_t logoSlotCount; uint8_t logoSlotCount;
uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in 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. // 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 }; 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 // Level-intro warp: how a star step looks at each distance BEHIND the
// bright band, as C64 colour indices (white, light grey, grey, dark // lit step, as C64 colour indices; further behind is unlit. One yellow
// grey, then black). A ring this far behind the band or further is // star per ray, as the C64's star sprites (colour 7, $4523) fly with no
// unlit, so a lit head with a short fading tail sweeps outward and the // trail.
// rest of the field stays dark. static const uint8_t kStarRamp[] = { 7u };
static const uint8_t kStarRamp[] = { 1u, 15u, 12u, 11u };
#define ST_STAR_RAMP_LEN (sizeof(kStarRamp) / sizeof(kStarRamp[0])) #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 StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx);
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx);
static void cacheCompile(StSpriteCacheT *e); static void cacheCompile(StSpriteCacheT *e);
static StSpriteCacheT *cacheFreeSlot(void); 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 void levelCelsEvict(void);
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); 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 bool spriteOnScreen(int16_t px, int16_t py);
static void starPaletteApply(void); static void starPaletteApply(void);
static void tileFromChunky(jlTileT *out, const uint8_t *chunky); static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
static bool tileHasPixels(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 // 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, // The cached cel for hardware sprite `idx` of the current frame, built
// built on first use from its VIC bitmap and colours. // on first use from its VIC bitmap and colours; NULL when there is
static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { // nothing to draw.
static StSpriteCacheT *cachedSprite(StSimT *sim, uint8_t idx) {
uint8_t ptr = sim->frame.ptr[idx]; uint8_t ptr = sim->frame.ptr[idx];
uint8_t color = drawColor(sim, idx); uint8_t color = drawColor(sim, idx);
uint8_t multi = (uint8_t)((sim->frame.multiMask & kStBit[idx]) != 0u ? 1u : 0u); 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. // 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) { 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; e->lastUse = gRender.cacheStamp;
return e->sprite; return e;
} }
for (k = 0u; k < ST_SPRITE_CACHE; k++) { for (k = 0u; k < ST_SPRITE_CACHE; k++) {
e = &gRender.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) { 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; e->lastUse = gRender.cacheStamp;
gRender.lastHit[idx] = e; gRender.lastHit[idx] = e;
return e->sprite; return e;
} }
// Pinned entries (prewarmed cab/exhaust/passenger cels) are the // Pinned entries (prewarmed cab/exhaust/passenger cels) are the
// costly-to-recompile ones and are never evicted; the least- // 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->used = false;
slot->compiled = false; slot->compiled = false;
} }
buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); if (!celCreate(slot, 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; return 0;
} }
// Nothing compiles mid-ride: a compile is ~0.5 s on the 65816, a // 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->used = true;
slot->pinned = gPrewarming; slot->pinned = gPrewarming;
gRender.lastHit[idx] = slot; 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, // Build a VIC bitmap's cel into a cache entry from the shared cel
// because the two-shift form of the multiply came back as a __mulhi3 // expansion (stCels.c, the same one the offline baker uses), cropped to
// library call on the 65816. // 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, // 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 // so the warning below stays about real misses. Only the title screen's
// decorations qualify: logoColorCycle recolours them every cycle step, // 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 uint8_t cellCol(uint16_t cell, uint8_t row) {
static const uint16_t kRowBase[ST_SCREEN_ROWS] = { static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
@ -425,8 +460,7 @@ static void dirtyBands(const StSimT *sim) {
} }
return; return;
} }
// A plain loop, not memset: 25 entries do not pay for the 65816 // One loop over both 25-entry arrays.
// libc's far-called byte loop.
for (row = 0u; row < ST_SCREEN_ROWS; row++) { for (row = 0u; row < ST_SCREEN_ROWS; row++) {
gRender.dirtyColMin[row] = 0xFFu; gRender.dirtyColMin[row] = 0xFFu;
gRender.dirtyColMax[row] = 0u; 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 // Public
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -718,13 +765,13 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
} }
} }
if (gRender.starMode) { if (gRender.starMode) {
// The warp is the only thing animating here, and it animates in // The warp animates in the palette: no cells change and no sprite
// the palette: no cells change, no sprite moves for it, so the // moves for it. The lit step follows the intro's own frame count,
// present that ends this frame copies no pixels at all. // so the stars keep the C64's speed whatever the frame rate.
gRender.starTick++; uint8_t phase = (uint8_t)((sim->introStepCount / ST_STAR_FRAMES_PER_STEP) & (ST_STAR_RING_COUNT - 1u));
if (gRender.starTick >= ST_STAR_STEP_FRAMES) {
gRender.starTick = 0u; if (phase != gRender.starPhase) {
gRender.starPhase = (uint8_t)((gRender.starPhase + 1u) & (ST_STAR_RING_COUNT - 1u)); gRender.starPhase = phase;
starPaletteApply(); starPaletteApply();
} }
} }
@ -752,7 +799,11 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = ST_HW_SPRITES; k > first; k--) { for (k = ST_HW_SPRITES; k > first; k--) {
StDrawnT *d = &gRender.slot[k - 1u]; StDrawnT *d = &gRender.slot[k - 1u];
if (d->drawn) { if (d->drawn) {
if (d->trusted) {
jlSpriteRestoreUnderTrusted(stage, &gRender.backup[k - 1u]);
} else {
jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]);
}
d->drawn = false; d->drawn = false;
} }
} }
@ -760,7 +811,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
for (k = first; k < ST_HW_SPRITES; k++) { for (k = first; k < ST_HW_SPRITES; k++) {
uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k);
StDrawnT *d = &gRender.slot[k]; StDrawnT *d = &gRender.slot[k];
jlSpriteT *sp; StSpriteCacheT *e;
int16_t px; int16_t px;
int16_t py; int16_t py;
if ((sim->frame.enableMask & kStBit[idx]) == 0u) { if ((sim->frame.enableMask & kStBit[idx]) == 0u) {
@ -771,11 +822,21 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) {
if (!spriteOnScreen(px, py)) { if (!spriteOnScreen(px, py)) {
continue; continue;
} }
sp = cachedSprite(sim, idx); e = cachedSprite(sim, idx);
if (sp == 0) { if (e == 0) {
continue; 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->drawn = true;
d->x = sim->frame.x[idx]; d->x = sim->frame.x[idx];
d->y = sim->frame.y[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. // The first unused cache slot, or NULL when the cache is full.
static StSpriteCacheT *cacheFreeSlot(void) { static StSpriteCacheT *cacheFreeSlot(void) {
uint8_t k; uint8_t k;
@ -936,15 +989,17 @@ bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
} }
if (got == n) { if (got == n) {
slot->sprite = cels[k]; 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 { } else {
const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr); const uint8_t *bm = levelSpriteBitmap(level, list[k].ptr);
if (bm == 0) { if (bm == 0) {
continue; continue;
} }
buildSpriteCel(bm, list[k].multi, list[k].color, list[k].mc0, list[k].mc1); if (!celCreate(slot, 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; continue;
} }
slot->compiled = false; 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 // 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 // count (a stale bake), so the caller falls back to the runtime JIT
// prewarm below. Every port stages its own bake (make/<port>.mk). // 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->mc0 = kStCels[i].mc0;
e->mc1 = kStCels[i].mc1; e->mc1 = kStCels[i].mc1;
e->level = 0xFFu; e->level = 0xFFu;
e->offX = 0u;
e->offY = 0u;
e->w = ST_SPRITE_PX;
e->h = ST_SPRITE_PX;
e->used = true; e->used = true;
e->pinned = true; e->pinned = true;
e->lastUse = 0u; e->lastUse = 0u;
@ -1071,11 +1134,16 @@ void stRenderLoading(jlSurfaceT *stage) {
jlStagePresent(); 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) { 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 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 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 }; 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; uint8_t done = 0u;
bool ok; bool ok;
uint8_t k; uint8_t k;
@ -1114,13 +1182,6 @@ bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
done++; done++;
loadingBar(stage, done, total); 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; sim->frame = saved;
gPrewarming = false; gPrewarming = false;
ok = prewarmReport(total); ok = prewarmReport(total);
@ -1206,11 +1267,10 @@ void stRenderTitleLogo(StSimT *sim) {
// Turn on the level intro's palette-cycled starfield. Call it after // Turn on the level intro's palette-cycled starfield. Call it after
// stRenderSceneChanged (which turns it back off) on entry to the level // stRenderSceneChanged (which turns it back off) on entry to the level
// intro. The cells were painted by stIntroEnter; from here the warp is // 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) { void stRenderIntroStars(void) {
gRender.starMode = true; gRender.starMode = true;
gRender.starPhase = 0u; gRender.starPhase = 0u;
gRender.starTick = 0u;
starPaletteApply(); starPaletteApply();
} }

28
stSim.h
View file

@ -100,8 +100,9 @@
// pixel rows. // pixel rows.
// //
// The contract between the two halves is just this colour range: // 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 // stTitle.c colours a star's cell ST_STAR_RING_FIRST + the step of its
// the centre), and the renderer cycles that range. These are C64 colour // 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 // 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. // mode, 4 and 5; the cab sprite adds 2, 6 and 7.
#define ST_STAR_RING_FIRST 8u #define ST_STAR_RING_FIRST 8u
@ -112,12 +113,14 @@
// something else must fail to compile here instead of quietly making // something else must fail to compile here instead of quietly making
// the warp skip rings. // the warp skip rings.
typedef char stStarRingCountIsPow2[((ST_STAR_RING_COUNT & (ST_STAR_RING_COUNT - 1u)) == 0u) ? 1 : -1]; 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 // A C64 star flies $20 frames ($4500) and the painted field marks every
// stars are the far ones and get the small glyph. The big glyph belongs // ST_STAR_FRAMES_PER_STEP of them, so the renderer steps the lit colour on
// to the outer rings, the stars about to rush past the viewer. // that many intro frames (introStepCount): the warp keeps the C64's speed at
#define ST_STAR_GLYPH_NEAR 0x2Au // '*', the outer rings // any frame rate.
#define ST_STAR_GLYPH_FAR 0x2Eu // '.', the inner rings #define ST_INTRO_STAR_FLIGHT 0x20u
#define ST_STAR_FAR_RINGS 5u // rings 0..4 are still distant #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 // VIC sprite block pointers the game selects by name. The cab's bit 0 is
// the landing gear, so each facing is a pair. // 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 uint8_t hookData[ST_HOOK_BYTES]; // $7D98..$7FFF as shipped
} StLevelT; } 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 { typedef struct {
uint8_t col; // $7175,X X low byte uint8_t col; // $7175,X X low byte
uint8_t msb; // $7185,X X high bit (any non-zero value = set) 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 enable; // $718E,X
uint8_t ptr; // $7197,X block pointer uint8_t ptr; // $7197,X block pointer
uint8_t color; // $719F,X uint8_t color; // $719F,X
uint8_t strideToPow2[2];
} StSpriteT; } StSpriteT;
typedef char stSpriteStrideIsPow2[((sizeof(StSpriteT) & (sizeof(StSpriteT) - 1u)) == 0u) ? 1 : -1];
// The frame the VIC last displayed ($4253 marshal + $4293 flush): the // The frame the VIC last displayed ($4253 marshal + $4293 flush): the
// renderer draws this and the collision test evaluates it. // renderer draws this and the collision test evaluates it.
@ -542,6 +551,7 @@ typedef struct {
uint8_t logoCycleAux; // $48A5 uint8_t logoCycleAux; // $48A5
uint8_t logoDiv; // $48A6 uint8_t logoDiv; // $48A6
uint8_t introLoopCount; // $4508 uint8_t introLoopCount; // $4508
uint8_t introStepCount; // port: intro frames run, for the painted warp
uint8_t introCabDx; // $450A uint8_t introCabDx; // $450A
uint8_t introCabDxTimer; // $450B uint8_t introCabDxTimer; // $450B
uint8_t spriteMc0; // $D025 uint8_t spriteMc0; // $D025

142
stTitle.c
View file

@ -14,8 +14,10 @@
// //
// The seven star sprites are replaced by a static starfield whose // 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 // 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 // how. The stars sit on the seven rays the C64's stars fly, where those
// sequence keeps its length and its cue; only the warp changes. // 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> #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_SPARKLE_TICKS 0x5Au
#define ST_TITLE_HOVER_COL 0x28u #define ST_TITLE_HOVER_COL 0x28u
#define ST_INTRO_STAR_PTR 0xDAu #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_END_ROW 0x94u
#define ST_INTRO_CAB_DX_RELOAD 0x28u #define ST_INTRO_CAB_DX_RELOAD 0x28u
// The palette-cycled starfield. Seven stars per ring so the bright band // The palette-cycled starfield. The C64 flies one star sprite along each
// stays the same weight all the way out, placed at fixed pseudo-random // of seven rays at these velocities ($450C / $4513, pixels a frame) for
// angles and clear of the three lines the intro screen writes (the level // $20 frames ($4500), then launches it again from the start. The painted
// name on row 12, and the demo hints on rows 3 and 24). // field puts a star where each flight is every ST_STAR_FRAMES_PER_STEP
typedef struct { // frames; the renderer lights one step of every ray at a time, so a star
uint8_t col; // hops out along its ray. A star is drawn where the sprite's image ($DA:
uint8_t row; // a 5x3 speck centred on pixel 11, 9 of the sprite) would be.
uint8_t ring; #define ST_INTRO_STARS 7u
} StStarT; #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[] = { static const int8_t kIntroStarDx[ST_INTRO_STARS] = { 0, 6, 6, 6, -6, -6, -6 };
{ 18u, 10u, 0u }, { 19u, 10u, 0u }, { 17u, 11u, 0u }, { 18u, 13u, 0u }, static const int8_t kIntroStarDy[ST_INTRO_STARS] = { -4, -4, 0, 4, 4, 0, -4 };
{ 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 void addToRow(StSimT *sim, uint8_t idx, uint8_t delta); static void addToRow(StSimT *sim, uint8_t idx, uint8_t delta);
static void logoColorCycle(StSimT *sim); static void logoColorCycle(StSimT *sim);
static void logoFlip(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 // $4113
@ -124,21 +119,60 @@ static void logoFlip(StSimT *sim) {
} }
// Paint the level-intro starfield into screen RAM, once. A star's cell // Paint the level-intro starfield into screen RAM, once. A star's cell is
// is coloured ST_STAR_RING_FIRST + its ring and never touched again -- // coloured ST_STAR_RING_FIRST + the step of its flight it marks, offset by
// the renderer animates the warp by cycling those colours, which costs // its ray's launch delay so the rays fire out of step as on the C64, and is
// no dirty rows at all. See ST_STAR_RING_FIRST in stSim.h. // never touched again -- the renderer animates the warp by cycling those
static void paintStarfield(StSimT *sim) { // colours, which costs no dirty rows at all (ST_STAR_RING_FIRST, stSim.h).
const StStarT *star; // Step 0 is the launch point, which every ray shares, and steps off the
uint8_t one[2]; // screen are left out, as are rows that carry the intro's text: on the C64
uint8_t k; // 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 // Before any star goes down, or the first star on a row would hide the
for (k = 0u; k < (uint8_t)ST_INTRO_STAR_COUNT; k++) { // rest of that row's.
star = &kIntroStar[k]; for (row = 0u; row < ST_SCREEN_ROWS; row++) {
one[0] = (star->ring < ST_STAR_FAR_RINGS) ? ST_STAR_GLYPH_FAR : ST_STAR_GLYPH_NEAR; textRow[row] = rowHasText(sim, row);
stSimDrawText(sim, star->col, star->row, one, (uint8_t)(ST_STAR_RING_FIRST + star->ring));
} }
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. // $4523..$4665 -- set up the level-name screen.
void stIntroEnter(StSimT *sim, const StLevelT *level) { void stIntroEnter(StSimT *sim, const StLevelT *level) {
uint8_t delay[ST_INTRO_STARS];
uint8_t k; uint8_t k;
sim->level = level; sim->level = level;
@ -161,20 +196,19 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->spr[7].row = 0xE4u; sim->spr[7].row = 0xE4u;
sim->introCabDxTimer = 0x14u; sim->introCabDxTimer = 0x14u;
sim->introCabDx = 0x01u; sim->introCabDx = 0x01u;
for (k = 0u; k < 7u; k++) { for (k = 0u; k < ST_INTRO_STARS; k++) {
uint8_t idx = (uint8_t)(6u - k); uint8_t idx = (uint8_t)(ST_INTRO_STARS - 1u - k);
sim->spr[idx].row = 0x8Cu; sim->spr[idx].row = ST_INTRO_STAR_ROW;
sim->spr[idx].msb = 0u; 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].color = 0x07u;
sim->spr[idx].ptr = ST_INTRO_STAR_PTR; sim->spr[idx].ptr = ST_INTRO_STAR_PTR;
sim->spr[idx].enable = 0u; sim->spr[idx].enable = 0u;
// $44F8/$4500 held each star's spawn timer and reload. Nothing // $44F8: each star's launch delay. The painted field uses it to
// reads them now that the warp is painted rather than flown, but // put the rays out of step, and the seven RNG draws MUST stay in
// the seven RNG draws MUST stay: they advance the shared RNG and // any case: they advance the shared RNG and the level that
// the level that follows draws from it, so dropping them would // follows draws from it.
// change the game that comes after the intro. delay[idx] = stSimRng(sim, ST_INTRO_STAR_DELAYS);
(void)stSimRng(sim, 0x20u);
} }
sim->borderColor = 0u; sim->borderColor = 0u;
sim->bgColor = 0u; sim->bgColor = 0u;
@ -188,12 +222,13 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) {
sim->charDirtyAll = true; sim->charDirtyAll = true;
sim->charDirtyCount = 0u; sim->charDirtyCount = 0u;
stSimDrawText(sim, 10u, 12u, level->name, 3u); stSimDrawText(sim, 10u, 12u, level->name, 3u);
paintStarfield(sim);
if (sim->demoMode != 0u && sim->postMortem == 0u) { if (sim->demoMode != 0u && sim->postMortem == 0u) {
stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u);
stSimDrawText(sim, 3u, 24u, kTextScreens, 4u); stSimDrawText(sim, 3u, 24u, kTextScreens, 4u);
} }
paintStarfield(sim, delay);
sim->introLoopCount = 0u; sim->introLoopCount = 0u;
sim->introStepCount = 0u;
} }
@ -203,6 +238,7 @@ bool stIntroStep(StSimT *sim) {
uint8_t k; uint8_t k;
stSimMarshal(sim); stSimMarshal(sim);
sim->introStepCount++;
sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u); sim->introLoopCount = (uint8_t)((sim->introLoopCount + 1u) & 3u);
addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]); addToRow(sim, 7u, (uint8_t)kIntroCabDy[sim->introLoopCount]);
if ((sim->introLoopCount & 1u) == 0u) { if ((sim->introLoopCount & 1u) == 0u) {