From a05958bd86ff085314cb17a5fa2fc808ec2face8 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 5 Oct 2026 16:47:00 -0500 Subject: [PATCH] More bug fixing and optimizations. --- assets/genC64Data.py | 5 +- spacetaxi.c | 90 ++++++++++++---- stAudio.c | 4 +- stCels.c | 9 +- stRender.c | 238 +++++++++++++++++++++++++++---------------- stSim.h | 28 +++-- stTitle.c | 142 ++++++++++++++++---------- 7 files changed, 338 insertions(+), 178 deletions(-) diff --git a/assets/genC64Data.py b/assets/genC64Data.py index 6c6f130..76b25bf 100644 --- a/assets/genC64Data.py +++ b/assets/genC64Data.py @@ -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 diff --git a/spacetaxi.c b/spacetaxi.c index c52c47e..d67d548 100644 --- a/spacetaxi.c +++ b/spacetaxi.c @@ -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); diff --git a/stAudio.c b/stAudio.c index d1ed9cb..1967593 100644 --- a/stAudio.c +++ b/stAudio.c @@ -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; diff --git a/stCels.c b/stCels.c index cb33c46..207f887 100644 --- a/stCels.c +++ b/stCels.c @@ -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])); diff --git a/stRender.c b/stRender.c index 3dd0e08..3d41935 100644 --- a/stRender.c +++ b/stRender.c @@ -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/.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(); } diff --git a/stSim.h b/stSim.h index 9f3f541..5991fe3 100644 --- a/stSim.h +++ b/stSim.h @@ -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 diff --git a/stTitle.c b/stTitle.c index ecc37d3..20beee3 100644 --- a/stTitle.c +++ b/stTitle.c @@ -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 @@ -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) {