// Space Taxi -- renderer: the simulation's screen RAM, colour RAM, // charset and sprite frame onto the JoeyLib stage. // // The screen is 40x25 character cells painted from the live charset // (the game edits glyphs at runtime: the transporter hatch, the title // logo flip-book, the laser beams). A cell goes to the stage in one // call: jlTilePasteGlyph takes the character's eight 1bpp charset rows // as they stand and colours them from the cell's colour RAM, so an // edited glyph needs no rebuild and a colour change costs nothing. // Only the cells the simulation listed as dirty are // repainted; a changed glyph dirties the cells showing it, found // inside the cell range each character was last painted in. The eight // VIC sprites are drawn from their bitmaps through a small cache of // JoeyLib sprites keyed on (pointer, colour, multicolour mode), with // save-under and LIFO restore. Sprite 0 has VIC priority, so the draw // order is sprite 7 first, sprite 0 last, and a sprite that has not // moved (and has nothing repainted under it) simply stays on the // stage: only it and everything drawn after it are undrawn and redrawn // when it changes. // // The title screen gets a special path (stRenderTitleLogo): its logo is // 103 cells of one character that the flip-book animates by copying one // of four frame glyphs over it, and a colour cycle that used to rewrite // the colour of 407 cells. Both were pure repaint cost. Instead the four // frames collapse into ONE tile whose pixels are drawn in reserved // palette slots -- one slot per set of frames a pixel belongs to -- so a // flip and a recolour are both a single palette write and no cell is // ever repainted. The title's decoration sprites, which cycle colour // with the logo, share one more reserved slot for the same reason. #include #include "spacetaxi.h" #include "stCels.h" #define ST_SPRITE_TILES 3u // The IIgs reaches its globals DBR-relative, so all of BSS must fit the // entry bank below the I/O window; the caches are sized down there. The // sprite cache must still hold every cel stRenderPrewarm builds (the // cab, exhaust and passenger sets plus the intro star) or a cel change // mid-play rebuilds and recompiles a sprite. Glyphs are direct-mapped // by character, so the count is a power of two. #if defined(__W65816__) #define ST_SPRITE_CACHE 48u #else #define ST_SPRITE_CACHE 72u #endif #define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #define ST_SPRITE_PX (8 * ST_SPRITE_TILES) // Palette slots the title logo animation owns. The title screen draws // in C64 colours 0, 1, 2, 6, 7, 11 and 12 only (plus colour 5 for the // "GET READY" banner that appears over it), so these are free while it // is up and the whole trick needs no per-scanline SCB band. #define ST_PAL_LOGO_SPR 3u // the decoration sprites' cycling colour #define ST_LOGO_PAL_SLOTS 7u // The first hardware sprite that cycles colour with the logo. #define ST_LOGO_SPR_FIRST 3u // Where the startup message sits, and its C64 colour (1 = white). #define ST_LOADING_ROW 12u #define ST_LOADING_COLOR 1u typedef struct { jlSpriteT *sprite; uint8_t ptr; uint8_t color; uint8_t multi; uint8_t mc0; uint8_t mc1; uint8_t level; // level index the bitmap came from (level sprites) bool used; bool pinned; // prewarmed moving cel: never evicted (its // compiled code is costly to re-emit on the 65816) uint32_t lastUse; // for least-recently-used eviction } StSpriteCacheT; // What is drawn in a draw slot (slot 0 = sprite 7 ... slot 7 = sprite 0). typedef struct { bool drawn; uint16_t x; uint8_t y; uint8_t ptr; uint8_t color; uint8_t multi; } StDrawnT; typedef struct { jlSurfaceT *stage; jlSurfaceT *scratch; // Title logo (stRenderTitleLogo): all four flip-book frames in one // tile, animated by rewriting logoSlotMask[]'s palette slots. bool logoMode; bool logoTileOk; // the live glyph is one of the frames uint8_t logoSlotCount; uint8_t logoSlotMask[ST_LOGO_PAL_SLOTS]; // frames each slot is lit in uint8_t logoFrame; // frame the palette currently shows uint8_t logoColor; // logo colour the palette currently shows uint8_t logoSprColor; // decoration-sprite colour it shows jlTileT logoTile; // Cell range (inclusive) each character has been painted in since // the last full repaint: where a changed glyph's cells can be. uint16_t glyphFirst[ST_CHARSET_CHARS]; uint16_t glyphLast[ST_CHARSET_CHARS]; // Column band of the cells about to be repainted, per character row // (min > max = the row is clean), the same shape as the library's // own per-row dirty bands. uint8_t dirtyColMin[ST_SCREEN_ROWS]; uint8_t dirtyColMax[ST_SCREEN_ROWS]; uint32_t cacheStamp; StSpriteCacheT cache[ST_SPRITE_CACHE]; StSpriteCacheT *lastHit[ST_HW_SPRITES]; // the entry each hardware sprite used last jlSpriteBackupT backup[ST_HW_SPRITES]; uint8_t backupMem[ST_HW_SPRITES][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2))); StDrawnT slot[ST_HW_SPRITES]; uint8_t lastPresentFrame; } StRenderStateT; static StRenderStateT gRender; // Set across stRenderPrewarm so a cel built there is pinned in the cache. static bool gPrewarming; // The VIC-II palette in register order, $0RGB. static const uint16_t kC64Palette[16] = { 0x0000, 0x0FFF, 0x0833, 0x06BB, 0x0839, 0x05A4, 0x0438, 0x0BC7, 0x0852, 0x0540, 0x0B66, 0x0555, 0x0777, 0x09E8, 0x076C, 0x09AA }; // Palette slots the logo tile's pixels are drawn in, one per distinct // set of flip-book frames a pixel belongs to. The four frames nest, so // four of these are used; the rest are headroom if the frames change. static const uint8_t kLogoPalSlot[ST_LOGO_PAL_SLOTS] = { 4u, 8u, 9u, 10u, 13u, 14u, 15u }; 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 uint8_t cellRow(uint16_t cell); static void collectGlyphs(StSimT *sim); static void dirtyBands(const StSimT *sim); static bool dirtyUnderSprite(int16_t px, int16_t py); static uint8_t drawColor(const StSimT *sim, uint8_t idx); static void dropCache(void); static bool loadPrecompiledCels(void); static bool logoBuildTile(const StSimT *sim); static uint8_t logoFrameIndex(const StSimT *sim); static void logoPaletteApply(const StSimT *sim, uint8_t frame); static void paintCells(jlSurfaceT *stage, StSimT *sim); static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by); static bool spriteOnScreen(int16_t px, int16_t py); static void tileFromChunky(jlTileT *out, const uint8_t *chunky); // Paint a VIC sprite bitmap onto the scratch surface's top-left 3x3 // tiles from the shared cel expansion (stCels.c, the same one the // offline baker uses), so a cel built here for the interpreter fallback // matches the precompiled .spc byte for byte. static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1) { uint8_t blob[ST_CEL_BYTES]; jlTileT tile; uint8_t tx; uint8_t ty; stCelBlob(bm, multi, color, mc0, mc1, blob); for (ty = 0u; ty < ST_SPRITE_TILES; ty++) { for (tx = 0u; tx < ST_SPRITE_TILES; tx++) { tileFromChunky(&tile, &blob[(ty * ST_SPRITE_TILES + tx) * TILE_BYTES]); jlTilePaste(gRender.scratch, tx, ty, &tile); } } } // The JoeyLib sprite for hardware sprite `idx` of the current frame, // built on first use from its VIC bitmap and colours. static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { uint8_t ptr = sim->frame.ptr[idx]; uint8_t color = drawColor(sim, idx); uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); uint8_t mc0 = multi ? sim->spriteMc0 : 0u; uint8_t mc1 = multi ? sim->spriteMc1 : 0u; uint8_t level = (ptr < ST_SPRITE_PTR_FIRST && sim->level != 0) ? sim->level->levelIndex : 0xFFu; StSpriteCacheT *slot = 0; StSpriteCacheT *e = gRender.lastHit[idx]; uint8_t k; gRender.cacheStamp++; // Most frames a hardware sprite shows the cel it showed last time. if (e != 0 && e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { e->lastUse = gRender.cacheStamp; return e->sprite; } for (k = 0u; k < ST_SPRITE_CACHE; k++) { e = &gRender.cache[k]; if (e->used && e->ptr == ptr && e->color == color && e->multi == multi && e->mc0 == mc0 && e->mc1 == mc1 && e->level == level) { e->lastUse = gRender.cacheStamp; gRender.lastHit[idx] = e; return e->sprite; } // Pinned entries (prewarmed cab/exhaust/passenger cels) are the // costly-to-recompile ones and are never evicted; the least- // recently-used unpinned slot is the victim, a free slot first. if (e->pinned) { continue; } if (slot == 0 || !e->used || (slot->used && e->lastUse < slot->lastUse)) { slot = e; } } if (slot == 0) { // Every slot is pinned (never happens with the prewarm set sized // below the cache): reuse the queried entry's own slot is unsafe, // so fail into the interpreter path. return 0; } if (slot->used) { jlSpriteDestroy(slot->sprite); slot->sprite = 0; slot->used = false; } buildSpriteCel(stSimSpriteBitmap(sim, ptr), multi, color, mc0, mc1); slot->sprite = jlSpriteCreateFromSurface(gRender.scratch, 0, 0, ST_SPRITE_TILES, ST_SPRITE_TILES); if (slot->sprite == 0) { return 0; } // Only the cab, exhaust and passenger move every tick; the codegen // arena is theirs. Everything else stays interpreted (drawn seldom). if (idx <= 2u) { (void)jlSpriteCompile(slot->sprite); } slot->lastUse = gRender.cacheStamp; slot->ptr = ptr; slot->color = color; slot->multi = multi; slot->mc0 = mc0; slot->mc1 = mc1; slot->level = level; slot->used = true; slot->pinned = gPrewarming; gRender.lastHit[idx] = slot; return slot->sprite; } // Block row of a screen cell: cell / 40, read from the 125 distinct // results of (cell >> 3) / 5. The old (x * 205) >> 10 identity avoided a // division but still called the 65816's software multiply, and this runs // twice for every repainted cell. static const uint8_t kCellRow[125] = { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24 }; static uint8_t cellRow(uint16_t cell) { return kCellRow[cell >> 3]; } // A changed glyph (the hatch, the logo flip, the laser beams) makes // every cell that shows it dirty. The simulation hands over a short list // of the characters it edited, so nothing scans the charset; the cell // search covers only the range the character was last painted in. The // glyph itself needs no invalidation -- the paste reads the live charset // every time. static void collectGlyphs(StSimT *sim) { uint8_t k; uint16_t cell; uint16_t last; if (sim->charDirtyAll) { sim->charDirtyAll = false; sim->charDirtyCount = 0u; return; } for (k = 0u; k < sim->charDirtyCount; k++) { uint8_t ch = sim->charDirtyList[k]; if (sim->dirtyAll) { continue; } last = gRender.glyphLast[ch]; for (cell = gRender.glyphFirst[ch]; cell <= last; cell++) { if (sim->screen[cell] != ch || sim->cellDirty[cell] != 0u) { continue; } sim->cellDirty[cell] = 1u; if (sim->dirtyCount < (uint16_t)(sizeof(sim->dirtyList) / sizeof(sim->dirtyList[0]))) { sim->dirtyList[sim->dirtyCount++] = cell; } else { sim->dirtyAll = true; break; } } } sim->charDirtyCount = 0u; } // Note where the cells about to be repainted are, as one column band // per character row. This used to be a single bounding box over every // dirty cell, and that box was wildly too coarse: the status row's // score and fare meters sit at opposite ends of the screen and one of // them moves on nearly every tick, so the box spanned the picture and // every sprite counted as sitting over repainted ground -- all eight // were undrawn and redrawn on about a third of all frames while only // the cab and its exhaust had actually moved. Per row the bands are // tight, and the rows a sprite covers are the only ones it tests. static void dirtyBands(const StSimT *sim) { uint16_t k; uint8_t row; if (sim->dirtyAll) { for (row = 0u; row < ST_SCREEN_ROWS; row++) { gRender.dirtyColMin[row] = 0u; gRender.dirtyColMax[row] = ST_SCREEN_COLS - 1u; } return; } // A plain loop, not memset: 25 entries do not pay for the 65816 // libc's far-called byte loop. for (row = 0u; row < ST_SCREEN_ROWS; row++) { gRender.dirtyColMin[row] = 0xFFu; gRender.dirtyColMax[row] = 0u; } for (k = 0u; k < sim->dirtyCount; k++) { uint16_t cell = sim->dirtyList[k]; uint8_t r = cellRow(cell); uint8_t col = (uint8_t)(cell - ((uint16_t)r << 5) - ((uint16_t)r << 3)); if (col < gRender.dirtyColMin[r]) { gRender.dirtyColMin[r] = col; } if (col > gRender.dirtyColMax[r]) { gRender.dirtyColMax[r] = col; } } } // True when a cell about to be repainted lies under the sprite drawn at // (px, py): its save-under backup would hold stale background, so it // has to be undrawn before the repaint and drawn again after. static bool dirtyUnderSprite(int16_t px, int16_t py) { int16_t x1 = (int16_t)(px + ST_SPRITE_W); int16_t y1 = (int16_t)(py + ST_SPRITE_PX); uint8_t row; uint8_t rowEnd; uint8_t colFirst; uint8_t colLast; if (x1 > SURFACE_WIDTH) { x1 = SURFACE_WIDTH; } if (y1 > SURFACE_HEIGHT) { y1 = SURFACE_HEIGHT; } // spriteOnScreen has already rejected anything fully off the stage, // so only the top/left overhang needs clamping to row/column 0. row = (uint8_t)(py < 0 ? 0 : (py >> 3)); rowEnd = (uint8_t)((y1 - 1) >> 3); colFirst = (uint8_t)(px < 0 ? 0 : (px >> 3)); colLast = (uint8_t)((x1 - 1) >> 3); for (; row <= rowEnd; row++) { if (gRender.dirtyColMin[row] <= colLast && gRender.dirtyColMax[row] >= colFirst) { return true; } } return false; } // The palette index a hardware sprite is drawn in. On the title screen // sprites 3..7 all follow the logo's colour cycle, so they share one // reserved slot: the cycle then recolours them with a palette write // instead of rebuilding five cels, and -- because their drawn state // stops changing -- without redrawing the sprites stacked above them. static uint8_t drawColor(const StSimT *sim, uint8_t idx) { if (gRender.logoMode && idx >= ST_LOGO_SPR_FIRST) { return ST_PAL_LOGO_SPR; } return sim->frame.color[idx]; } static void dropCache(void) { uint8_t k; for (k = 0u; k < ST_SPRITE_CACHE; k++) { if (gRender.cache[k].used) { jlSpriteDestroy(gRender.cache[k].sprite); gRender.cache[k].sprite = 0; gRender.cache[k].used = false; } } } // Collapse the four flip-book frame glyphs into one tile: every pixel // is drawn in the reserved palette slot that stands for the set of // frames it is set in (no frame = colour 0, the background). Animating // the logo is then a palette write per flip instead of a repaint of // every cell showing it. Returns false if the frames need more distinct // sets than there are reserved slots, leaving the ordinary per-cell // repaint to draw the flip book. static bool logoBuildTile(const StSimT *sim) { uint8_t slotOfMask[1u << ST_LOGO_FRAMES]; uint8_t chunky[TILE_BYTES]; uint8_t row; uint8_t col; memset(slotOfMask, 0, sizeof(slotOfMask)); gRender.logoSlotCount = 0u; for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { uint8_t *out = &chunky[row * TILE_BYTES_PER_ROW]; for (col = 0u; col < TILE_PIXELS_PER_SIDE; col++) { uint8_t mask = 0u; uint8_t f; uint8_t slot; for (f = 0u; f < ST_LOGO_FRAMES; f++) { if (((sim->charset[ST_LOGO_FRAME_FIRST + f][row] >> (7u - col)) & 1u) != 0u) { mask |= (uint8_t)(1u << f); } } if (mask != 0u && slotOfMask[mask] == 0u) { if (gRender.logoSlotCount == ST_LOGO_PAL_SLOTS) { return false; } slotOfMask[mask] = kLogoPalSlot[gRender.logoSlotCount]; gRender.logoSlotMask[gRender.logoSlotCount] = mask; gRender.logoSlotCount++; } slot = slotOfMask[mask]; if ((col & 1u) == 0u) { out[col >> 1] = (uint8_t)(slot << 4); } else { out[col >> 1] |= slot; } } } tileFromChunky(&gRender.logoTile, chunky); return true; } // Which flip-book frame the live logo glyph holds, or ST_LOGO_FRAMES if // it matches none of them (then the logo tile does not stand for what // the simulation is showing and the cells must be repainted normally). static uint8_t logoFrameIndex(const StSimT *sim) { uint8_t f; for (f = 0u; f < ST_LOGO_FRAMES; f++) { if (memcmp(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_FRAME_FIRST + f], 8u) == 0) { return f; } } return ST_LOGO_FRAMES; } // Show flip-book frame `frame` in the logo's current colour: every // reserved slot lights up in the colour if that frame is in its set and // goes to the background colour if it is not. The decoration sprites' // slot follows their own colour, which the cycle sets one step behind // the logo's (the title's sprite table overwrites it on entry). static void logoPaletteApply(const StSimT *sim, uint8_t frame) { uint16_t pal[SURFACE_COLORS_PER_PALETTE]; uint16_t on = kC64Palette[sim->logoColor & 15u]; uint16_t off = kC64Palette[sim->bgColor & 15u]; uint8_t k; jlPaletteGet(gRender.stage, 0u, pal); for (k = 0u; k < gRender.logoSlotCount; k++) { pal[kLogoPalSlot[k]] = (((gRender.logoSlotMask[k] >> frame) & 1u) != 0u) ? on : off; } pal[ST_PAL_LOGO_SPR] = kC64Palette[sim->frame.color[ST_LOGO_SPR_FIRST] & 15u]; jlPaletteSet(gRender.stage, 0u, pal); gRender.logoFrame = frame; gRender.logoColor = sim->logoColor; gRender.logoSprColor = sim->frame.color[ST_LOGO_SPR_FIRST]; } // Repaint the dirty cells: the list, or everything after an overflow // or a scene change (which also restarts the per-character cell ranges). static void paintCells(jlSurfaceT *stage, StSimT *sim) { uint16_t cell; uint16_t k; uint8_t bx; uint8_t by; if (sim->dirtyAll) { memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst)); memset(gRender.glyphLast, 0, sizeof(gRender.glyphLast)); cell = 0u; for (by = 0u; by < ST_SCREEN_ROWS; by++) { for (bx = 0u; bx < ST_SCREEN_COLS; bx++) { pasteCell(stage, sim, cell, bx, by); cell++; } } memset(sim->cellDirty, 0, ST_SCREEN_CELLS); sim->dirtyCount = 0u; sim->dirtyAll = false; return; } for (k = 0u; k < sim->dirtyCount; k++) { cell = sim->dirtyList[k]; by = cellRow(cell); bx = (uint8_t)(cell - ((uint16_t)by << 5) - ((uint16_t)by << 3)); pasteCell(stage, sim, cell, bx, by); sim->cellDirty[cell] = 0u; } sim->dirtyCount = 0u; } // Paint one cell: the character's charset rows coloured from colour RAM // over the background. Also notes the cell in the character's range, // which is where a changed glyph's cells are looked for. static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by) { uint8_t chr = sim->screen[cell]; if (cell < gRender.glyphFirst[chr]) { gRender.glyphFirst[chr] = cell; } if (cell > gRender.glyphLast[chr]) { gRender.glyphLast[chr] = cell; } // On the title the logo's cells carry all four flip-book frames at // once; the palette decides which one shows, so they are pasted as // they are and never touched again. if (gRender.logoTileOk && chr == ST_LOGO_CHAR) { jlTilePaste(stage, bx, by, &gRender.logoTile); return; } jlTilePasteGlyph(stage, bx, by, sim->charset[chr], sim->color[cell], sim->bgColor); } static bool spriteOnScreen(int16_t px, int16_t py) { return px < SURFACE_WIDTH && py < SURFACE_HEIGHT && px > -ST_SPRITE_W && py > -(int16_t)ST_SPRITE_PX; } // A chunky 8x8 tile (four bytes per row, high nibble = left pixel) as // this port's jlTileT: a straight copy on the chunky ports, drawn and // snapped through the scratch surface on the planar ones. static void tileFromChunky(jlTileT *out, const uint8_t *chunky) { #if defined(JOEYLIB_NATIVE_CHUNKY) memcpy(out->pixels, chunky, TILE_BYTES); #else uint8_t row; uint8_t k; for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { for (k = 0u; k < TILE_BYTES_PER_ROW; k++) { uint8_t b = chunky[row * TILE_BYTES_PER_ROW + k]; jlDrawPixel(gRender.scratch, (int16_t)(k * 2u), (int16_t)row, (uint8_t)(b >> 4)); jlDrawPixel(gRender.scratch, (int16_t)(k * 2u + 1u), (int16_t)row, (uint8_t)(b & 0x0Fu)); } } jlTileSnap(gRender.scratch, 0u, 0u, out); #endif } // --------------------------------------------------------------------------- // Public // --------------------------------------------------------------------------- void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { uint8_t first = ST_HW_SPRITES; uint8_t k; // Find the first draw slot whose sprite changed, vanished, or sits // over cells about to be repainted; it and every later slot are // undrawn (last first) and redrawn below, the rest stay put. if (gRender.logoMode) { // The flip book only ever swaps the logo glyph for one of the // four frames the logo tile already carries: recolour the // reserved slots and drop the glyph's repaint on the floor. uint8_t frame = logoFrameIndex(sim); gRender.logoTileOk = (frame < ST_LOGO_FRAMES); if (gRender.logoTileOk) { // The flip book only swapped in a frame the logo tile already // carries, so drop the repaint it asked for. if (sim->charDirtyCount == 1u && sim->charDirtyList[0] == ST_LOGO_CHAR) { sim->charDirtyCount = 0u; } if (frame != gRender.logoFrame || sim->logoColor != gRender.logoColor || sim->frame.color[ST_LOGO_SPR_FIRST] != gRender.logoSprColor) { logoPaletteApply(sim, frame); } } } collectGlyphs(sim); dirtyBands(sim); for (k = 0u; k < ST_HW_SPRITES; k++) { uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); StDrawnT *d = &gRender.slot[k]; uint8_t multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); int16_t px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); int16_t py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); bool want = (sim->frame.enableMask & (uint8_t)(1u << idx)) != 0u && spriteOnScreen(px, py); bool same = (want == d->drawn); if (same && want) { same = (d->x == sim->frame.x[idx] && d->y == sim->frame.y[idx] && d->ptr == sim->frame.ptr[idx] && d->color == drawColor(sim, idx) && d->multi == multi); } if (same && want && dirtyUnderSprite(px, py)) { same = false; } if (!same) { first = k; break; } } for (k = ST_HW_SPRITES; k > first; k--) { StDrawnT *d = &gRender.slot[k - 1u]; if (d->drawn) { jlSpriteRestoreUnder(stage, &gRender.backup[k - 1u]); d->drawn = false; } } paintCells(stage, sim); for (k = first; k < ST_HW_SPRITES; k++) { uint8_t idx = (uint8_t)(ST_HW_SPRITES - 1u - k); StDrawnT *d = &gRender.slot[k]; jlSpriteT *sp; int16_t px; int16_t py; if ((sim->frame.enableMask & (uint8_t)(1u << idx)) == 0u) { continue; } px = (int16_t)((int16_t)sim->frame.x[idx] - ST_SPRITE_X_ORIGIN); py = (int16_t)((int16_t)sim->frame.y[idx] - ST_SPRITE_Y_ORIGIN); if (!spriteOnScreen(px, py)) { continue; } sp = cachedSprite(sim, idx); if (sp == 0) { continue; } jlSpriteSaveAndDraw(stage, sp, px, py, &gRender.backup[k]); d->drawn = true; d->x = sim->frame.x[idx]; d->y = sim->frame.y[idx]; d->ptr = sim->frame.ptr[idx]; d->color = drawColor(sim, idx); d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); } { // Sync to the retrace unless the frame already spans more than // one: a late frame goes out at once rather than waiting again. uint8_t now = (uint8_t)jlFrameCount(); if ((uint8_t)(now - gRender.lastPresentFrame) < 2u) { jlWaitVBL(); } jlStagePresent(); gRender.lastPresentFrame = (uint8_t)jlFrameCount(); } } void stRenderInit(jlSurfaceT *stage) { uint8_t k; memset(&gRender, 0, sizeof(gRender)); memset(gRender.glyphFirst, 0xFF, sizeof(gRender.glyphFirst)); gRender.stage = stage; for (k = 0u; k < ST_HW_SPRITES; k++) { gRender.backup[k].bytes = gRender.backupMem[k]; } jlPaletteSet(stage, 0u, kC64Palette); jlScbSetRange(stage, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); jlSurfaceClear(stage, 0u); gRender.scratch = jlSurfaceCreate(); if (gRender.scratch != 0) { jlPaletteSet(gRender.scratch, 0u, kC64Palette); jlScbSetRange(gRender.scratch, 0u, (uint16_t)(SURFACE_HEIGHT - 1u), 0u); jlSurfaceClear(gRender.scratch, 0u); } } // Build (and compile) the cels the game draws every screen -- cab, // exhaust, passenger, wreck, warp, intro cab and star -- up front, so // no frame pays for a sprite build mid-play. A bar on the stage shows // progress on the slower ports. // Load the offline-baked, already-compiled cel bank into the pinned // cache so no cel is JIT-compiled at boot (each 65816 compile costs // ~0.4 s). Every kStCels entry becomes a pinned cache slot keyed exactly // as the play lookups: system ptr (level 0xFF), the level-wide colours. // Returns false if the .spc is missing or built for another target/shift // count (other ports, or a stale bake), so the caller falls back to the // runtime JIT prewarm below. Its cross-platform contract makes this a // no-op everywhere but the IIgs, where the bake is wired. static bool loadPrecompiledCels(void) { jlSpriteT *cels[64]; uint16_t n; uint16_t i; if (kStCelCount > (uint16_t)(sizeof(cels) / sizeof(cels[0])) || kStCelCount > ST_SPRITE_CACHE) { return false; } n = jlSpriteBankLoadPrecompiled("sprites/staxicels.spc", cels, kStCelCount, 0); if (n < kStCelCount) { for (i = 0u; i < n; i++) { jlSpriteDestroy(cels[i]); } return false; } for (i = 0u; i < n; i++) { StSpriteCacheT *e = &gRender.cache[i]; e->sprite = cels[i]; e->ptr = kStCels[i].ptr; e->color = kStCels[i].color; e->multi = kStCels[i].multi; e->mc0 = kStCels[i].mc0; e->mc1 = kStCels[i].mc1; e->level = 0xFFu; e->used = true; e->pinned = true; e->lastUse = 0u; } return true; } // "LOADING..." centred on an otherwise black stage. The charset is the // game's own, indexed by ASCII (the C64 font's letters sit at their // ASCII codes), so this needs no font surface and no simulation state. void stRenderLoading(jlSurfaceT *stage) { static const char kLoading[] = "LOADING..."; const uint8_t *charset = stC64Charset(); uint8_t col = (uint8_t)((ST_SCREEN_COLS - (sizeof(kLoading) - 1u)) / 2u); uint8_t k; jlSurfaceClear(stage, 0u); for (k = 0u; k < (uint8_t)(sizeof(kLoading) - 1u); k++) { jlTilePasteGlyph(stage, (uint8_t)(col + k), ST_LOADING_ROW, &charset[(uint8_t)kLoading[k] * 8u], ST_LOADING_COLOR, 0u); } jlStagePresent(); } void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 }; static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 }; static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 }; uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u); uint8_t done = 0u; uint8_t k; StFrameT saved; // Precompiled bank: instant, no per-cel JIT. Falls through to the // runtime build below on any port without a matching .spc. if (loadPrecompiledCels()) { return; } saved = sim->frame; gPrewarming = true; sim->frame.multiMask = 0x07u; sim->spriteMc0 = 0x02u; sim->spriteMc1 = 0x07u; for (k = 0u; k < sizeof(kCabPtrs); k++) { sim->frame.ptr[0] = kCabPtrs[k]; sim->frame.color[0] = 0x06u; (void)cachedSprite(sim, 0u); done++; jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); jlStagePresent(); } for (k = 0u; k < sizeof(kPassPtrs); k++) { sim->frame.ptr[1] = kPassPtrs[k]; sim->frame.color[1] = 0x06u; (void)cachedSprite(sim, 1u); done++; jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); jlStagePresent(); } for (k = 0u; k < sizeof(kFlamePtrs); k++) { sim->frame.ptr[2] = kFlamePtrs[k]; sim->frame.color[2] = 0x07u; (void)cachedSprite(sim, 2u); done++; jlFillRect(stage, 60, 110, (uint16_t)((uint16_t)done * 200u / total), 6u, 1u); jlStagePresent(); } // The intro star is a hires sprite in colour 7. Build it as a moving // (idx <= 2) cel so it compiles: the level-name intro draws seven of // them every frame, and the interpreter cannot keep up. sim->frame.multiMask = 0u; sim->frame.ptr[2] = 0xDAu; sim->frame.color[2] = 0x07u; (void)cachedSprite(sim, 2u); sim->frame = saved; gPrewarming = false; jlFillRect(stage, 60, 110, 200u, 6u, 0u); } // A new scene: nothing drawn is valid any more. void stRenderSceneChanged(StSimT *sim) { uint8_t k; if (gRender.logoMode) { // The logo's reserved slots go back to being C64 colours. gRender.logoMode = false; gRender.logoTileOk = false; jlPaletteSet(gRender.stage, 0u, kC64Palette); } for (k = 0u; k < ST_HW_SPRITES; k++) { gRender.slot[k].drawn = false; } stSimDirtyAll(sim); sim->charDirtyAll = true; sim->charDirtyCount = 0u; } // Turn on the title's palette-animated logo. Call it after // stRenderSceneChanged (which turns it back off) on entry to the title // screen; from then on a flip-book step and a colour cycle both cost // one palette write and no cell repaint at all. void stRenderTitleLogo(StSimT *sim) { uint8_t frame; if (!logoBuildTile(sim)) { return; } frame = logoFrameIndex(sim); gRender.logoMode = true; gRender.logoTileOk = (frame < ST_LOGO_FRAMES); logoPaletteApply(sim, gRender.logoTileOk ? frame : 0u); stSimDirtyAll(sim); } void stRenderShutdown(void) { dropCache(); if (gRender.scratch != 0) { jlSurfaceDestroy(gRender.scratch); gRender.scratch = 0; } }