From 46baf7ad5fc01ec038c7d4ce3d9602512f93ef97 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 17 Sep 2026 17:46:47 -0500 Subject: [PATCH] Space Taxi work. --- assets/font.png | Bin 1379 -> 129 bytes assets/tiles/tbank0.png | Bin 1879 -> 129 bytes assets/tiles/tbank1.png | Bin 1201 -> 129 bytes assets/tiles/tbank2.png | Bin 1237 -> 129 bytes assets/tiles/tilebank0.png | Bin 1039 -> 129 bytes spacetaxi.c | 15 +- spacetaxi.h | 12 +- stHooks.c | 11 +- stRender.c | 413 ++++++++++++++++++++++++++++--------- stSim.c | 63 +++++- stSim.h | 54 ++++- stTitle.c | 25 ++- 12 files changed, 460 insertions(+), 133 deletions(-) diff --git a/assets/font.png b/assets/font.png index 6a55e577f78e58cc120c0bc91bc950e677f93d08..3bad103df151f7681f7093013aa939755ce1bbe2 100644 GIT binary patch literal 129 zcmWN_K@P$o5CFh?U%>|~Qec7pX4zeYM5Phz!Pnc;JaRqY!|Nq}%X_UTg zZ@2xl&;_0jhQ0gG6?KPbhC4Cr$V>_d2&ky2nDU^rBJ;@n`SXG5N5N0r{yQ1&y&X(ffb@A9VH&1$Z$KQ|N z{cBGAzMObo_NgeN52MYZ$xc6S)Yx^zOCNAA2x7?oXR9=ee?iIvfu#~&-#!K%F=pM# z^Emp^aVy8pgG{H^o*m&RA_ z-o4>5&)H+Ye$4l}Ys9}x_~QB5Q~K|3@BbUU;{*CMJO!ybM>v!Sy zFWzb~>>p-q?N?v5UB&#d%_3Ln51W`gI%D;7b{zWkuk`4JP_H%iPi3Zscy?RJPTjUX z_1LA(mwt7})~4k6?a4j2Z}Oyi-Nm=!L}C?^BNC5PT<-hq-x+vGA?Rr7)L%(#dS3gM zL`=C7IrYz{p3^)z*H-viOE8t*%6jH=E06WVCiZW(8QP97|GirCKDK^B_vi0#pQtq4 zm9^wfID5c;@{6mMenNS7j~u@@xhi(fmj%*`zlBA|nsezJZ`gCc#P)N-#n}A2`|V4P z?>F23XJzGE^EJkgN}`VK-*sf6|I>BtF7I-EC+E$|Ix_p}fv>9;$m@O&)HJ_YaXde3 zdZYH@u=$xEx2it)AA9I;ORChq-TKV;KF)q8wc%O4=EUg^I|Fu0{*3;1V87wLS$2$M drP>3xf3n(FvhbeZ7Tg0ez|+;wWt~$(695LND);~Z diff --git a/assets/tiles/tbank0.png b/assets/tiles/tbank0.png index d07339470ab795de8353a5ace644c50ab2f2ab89..fc5ee4774f1ddebcd5edfbcc235443ef34853fce 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{DsX@=to3k_KT$rDL!QPp@zDE`CcNFWu%mWi#g9=j~Bt`QJWe zq46|wGMUS&(GM+ccQI{+9A$zMIde`&f$UO9&JucH^bXdD7_ioT#cUBNI4IyL+DeSf LO8S?zg^cP4w-zT5 literal 1879 zcma)-i9Zv30Ed4Y8*}9>v~o?ZLayc2vE>|NdzpJ!k>prJVL6uE>V3WBUYT;`E=MM@ za`QTJ)M}kblGrLN@p^mzgZKOSJkRqdd^3nnwvuAXVgLY1oSl{PuhRcRSm>7zOUDU* z#gK?6S^p}V&Hm;8x!BpIV*tQsaaLGT45Pvmk@Lz>G+Bkcb7F5+C1mI;%9VfP&zZqP zK~AHY?IVf%f`i*PL|5L-#b%T|w{l9f*{{Q^@L%s}ghacPL`HZLI~FK!l6o;o9(8%q z_;O*8&`;*qhZ*QFpEwa^=cG5YF*&OKGwQ;L&OXoi7;#=9CQDn1=C|hYZhT<$26Z!rzOIbDrB@M| zb784J<`E!k#yH&2cT_lX?t)=C6+)Z%AtB4!OnX@ch}e^F$Ft)lqv878sn0Dwv0apj zwl`rrga`Xr5#p^`ViBIT9Lk7ApM!0P%%X`uZOPIs^75c7E7pC~38Ea<-w_~{?VCr> z?aiTg*yGwmx+5$ZpF~!Vb zj{kv_eEnPocDZi0U=B_+cJK2|HQ7WW&5d+#`pf3jDnb*~Ug%ExMjoAVwy6CHW2RV7 z5sx|O?osbG%6f?gS_H%a3*n(ikwyEi2x4&2fgTmGgg-0|33FLX)wx8o|N9u}h~sYg za_e(1O9L4T2dntoGt1tLP}hv_`ll^02?TMSQa8Vj{kY*RJeXSP6k|uuwFCgQA~TE3 zcp(ds`BxZ zQC^S7R)bO*1A8`~>|czRMtKf0?y?*P_a_07^ylSpk6Rd^d8onISs4WhARjv5;5k1K z0e#o{UD$3ScU5-YXe|xJ+H`wA_85jab^CfRBD^e7e7mKA8aV&0;D=yCmN0dD^e~~P z%9l7#y&9w#TOSFuwJPTBnH*rtF;nx3fEGP4!zWk%++nLpczU*X%7#=EPoXwh8LrR? zh{+nk=fQXhVbarr$dg8ha%G{(Q76DWOxnvFlC5?!<^HymbjU$>FJ!NBRxW2c6|Gdy zTwII>%jg2-!XWI=;m5m2@9UA6iK3Rtd9-NC<{?HP?T^2g|N5u(y8XiqVZ%ho#2oTT zRT?Qk0_0zXhtK%3cv^au-&NA{H)QtZT&6~W|1*Wi-k zz-#%FHfUG#RA9+|)irU=v|cwQVDu@x>2_Q*rn8kCNI)HYxBqyveyV7ew*|SQ=yVqY z6x%XAe4bdECic*3lMZg*WMU;Z|i&<_@V*XJVTH{oEo&%qaf&YYwqBnFhc#e zdZCYgKpW;Hf-z0mVI~o|C>h}nJk-?jt7LY6){!ePhJT&D8$Z-&>&@_oo{y}ZdcTL9 zIVr%0Gs{C{*Acc3>4WB=JyXN=%$-B8f~FJ70(XH-zn;_wjTz{W(!zzzP}Px3z8Ss&&}4;9MP7rX@3A)ihkDj6BxXk%+j;?=0z}5PXY+(IW-Q+q z6(6GswLP^_4X+f6ypol%#u^y0yE z*2GsI5*gJmrZ$p06BrRTb=&bS2Zje9wW0!s1J(mB9%yhN3EEN0dW_NyGpE diff --git a/assets/tiles/tbank1.png b/assets/tiles/tbank1.png index 58bed90952ea020c71ff9a039e9f9b17a2830154..f83877be921db150429478e2ba014994ad54435d 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFhCuiyiQ#bL{D2AC15R8m56@b%i4zUVXic+0ldF?XdNechfsxBu5Ap+^7HCL!E7RZ`(wp2|FHdhYdxiF#uc8Llm M7PEgz8mo=?0r0gbga7~l literal 1201 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~;$#u6*%w%9-W(e>JaRqY!|Nq}%X_UTg zZ@2xl&;_0jhQ0gG6?KPbhC4Cr$V>_d2&ky2nDU^rBJ;@n`SXG5N5N2wOnK|LOPm1$P{X-L13vWRc&dbrG(A_AFRxVyd-z*ZT{0 zQPuw*7Q{rBMN600KfG}9;MG^@HaYqArh)&Wigw*%zvE`HPoMEy%dNo0_g^nA-8E-% zUSq4EZ*+A-;%UVLg|iBn<#`MCEsN91?fDzOrrq$xYRPq_DnmP=T%E1zjj^S*cS zJ9_Gv=kt8nvN8OD*n>FZ$Jr|m1Z-435H*>>J@Nco-2)OSy06cjXP@EsI+;~(ZVy+C zUc&4{TlED`Hq?bRCfn57{=6V^F0C}G@%yRpca|Z`v~Mu$d@icwDu};*FOvB)gZ#P! zrF?6%6MhMBh8mu+7>JjQO!qs`m%K9>LW zj7#b-GmetXWmRukk17uRE`VNA78ai(LUa;%v$R4$DCB^cfC3RPWA>QPZkZ`t&LHTE MsL{SfTek-F1D~cQP5=M^ literal 1237 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~;$#u6*%w%9-W(e>JaRqY!|Nq}%X_UTg zZ@2xl&;_0jhQ0gG6?KPbhC4Cr$V>_d2&ky2nDU^rBJ;@n`SXG5N5N0=iZ7XccOH1b1>ff1q{fg}_nYeF74I8H!^)JfZDrWV{ z;d@$=)a4gJcjdeo-MWhZ&Aq+aHNWYU@n0uaCzhR2uo_8qc+WsrfJ;yEg2qs8JUU{#q>t4z> z&8@g^ zH<~ZSMZ)M5aHgzmZ& MQSkUxX|#*t50#`QfdBvi literal 1039 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~;$#u6*%w%9-W(e>JaRqW27|PFp2!=El zhIj86IyyRb@80c__P@O2|9>Fozsvs)hW{W40$}x{#ApZ%lMpZx;|&IS<&>w3V@SoV zw^t5wH5&-HUKH^))%fwhKH7Qy?7W<#@6I&}Zra-u>}6B_*ZAEjyYmvsUG6gP50x|a zf0tqUCvkx-jw6d%UqoB}V9oY%Q~loCIG$charset[0x92u + (k >> 3)][k & 7u] = 0u; } - sim->charDirty[0x92u] = 1u; - sim->charDirty[0x93u] = 1u; + stSimMarkChar(sim, 0x92u); + stSimMarkChar(sim, 0x93u); for (i = 0u; i < 8u; i++) { uint8_t state = HK(sim, 0x7DAC + i); uint8_t col = HK(sim, 0x7DCD + i); @@ -376,8 +377,8 @@ static void hookWPerTick(StSimT *sim) { // and $2C98 + (8 - c) (char $93 rows 7..1) get a beam bar. sim->charset[0x91u + ((7u + c) >> 3)][(7u + c) & 7u] = 0x3Cu; sim->charset[0x93u][8u - c] = 0x3Cu; - sim->charDirty[0x92u] = 1u; - sim->charDirty[0x93u] = 1u; + stSimMarkChar(sim, 0x92u); + stSimMarkChar(sim, 0x93u); if ((c & 1u) == 0u) { return; } @@ -628,7 +629,7 @@ static void hookOTramp0(StSimT *sim) { uint8_t b = sim->charset[0x6Fu][k]; sim->charset[0x6Fu][k] = (uint8_t)((b >> 1) | (b << 7)); } - sim->charDirty[0x6Fu] = 1u; + stSimMarkChar(sim, 0x6Fu); } diff --git a/stRender.c b/stRender.c index 6a1f104..65fce0e 100644 --- a/stRender.c +++ b/stRender.c @@ -3,11 +3,11 @@ // // 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 glyph is one mono JoeyLib tile -// (set bits = non-zero pixels), cached by shape and coloured on the way -// to the stage by jlTilePasteMono from the cell's colour RAM, so a -// colour change never rebuilds a tile and an animated glyph is built -// once per shape. Only the cells the simulation listed as dirty are +// 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 @@ -17,6 +17,16 @@ // 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 @@ -32,15 +42,23 @@ // by character, so the count is a power of two. #if defined(__W65816__) #define ST_SPRITE_CACHE 48u - #define ST_GLYPH_CACHE 64u #else #define ST_SPRITE_CACHE 72u - #define ST_GLYPH_CACHE 256u #endif #define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_SPRITE_TILES, ST_SPRITE_TILES) #define ST_SPRITE_PX (8 * ST_SPRITE_TILES) -// glyphFirst[] value for a character shown in no cell. -#define ST_NO_CELL 0xFFFFu +// 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; @@ -68,18 +86,27 @@ typedef struct { } StDrawnT; typedef struct { + jlSurfaceT *stage; jlSurfaceT *scratch; - // Glyphs as mono tiles (set bits = non-zero pixels, coloured per cell - // by jlTilePasteMono), direct-mapped by character and valid while - // the bits match. Parallel arrays: power-of-two element sizes keep - // the 65816 indexing to shifts. - uint8_t glyphBits[ST_GLYPH_CACHE][8]; - jlTileT glyphTiles[ST_GLYPH_CACHE]; - bool glyphUsed[ST_GLYPH_CACHE]; + // 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 @@ -100,15 +127,24 @@ static const uint16_t kC64Palette[16] = { 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 bool loadPrecompiledCels(void); static uint8_t cellRow(uint16_t cell); static void collectGlyphs(StSimT *sim); -static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1); +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 const jlTileT *glyphTile(const StSimT *sim, uint8_t chr); +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); @@ -139,7 +175,7 @@ static void buildSpriteCel(const uint8_t *bm, uint8_t multi, uint8_t color, uint // 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 = sim->frame.color[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; @@ -206,33 +242,50 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) { } -// Block row of a screen cell without a division: cell / 40 is -// (cell / 8) / 5, and x * 205 / 1024 equals x / 5 for every x below 125. +// 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 (uint8_t)(((uint16_t)(cell >> 3) * 205u) >> 10); + return kCellRow[cell >> 3]; } // A changed glyph (the hatch, the logo flip, the laser beams) makes -// every cell that shows it dirty. The glyph cache self-invalidates on -// the bitmap compare, so nothing is rebuilt here; the search covers -// only the cell range the character was last painted in. +// 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) { - uint16_t ch; + uint8_t k; uint16_t cell; uint16_t last; - for (ch = 0u; ch < ST_CHARSET_CHARS; ch++) { - if (sim->charDirty[ch] == 0u) { - continue; - } - sim->charDirty[ch] = 0u; + 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] != (uint8_t)ch || sim->cellDirty[cell] != 0u) { + if (sim->screen[cell] != ch || sim->cellDirty[cell] != 0u) { continue; } sim->cellDirty[cell] = 1u; @@ -244,46 +297,96 @@ static void collectGlyphs(StSimT *sim) { } } } + sim->charDirtyCount = 0u; } -// Bounding box (pixels, x1/y1 exclusive) of the cells about to be -// repainted. Empty when x1 <= x0. -static void dirtyRect(const StSimT *sim, int16_t *x0, int16_t *y0, int16_t *x1, int16_t *y1) { +// 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) { - *x0 = 0; - *y0 = 0; - *x1 = SURFACE_WIDTH; - *y1 = SURFACE_HEIGHT; + for (row = 0u; row < ST_SCREEN_ROWS; row++) { + gRender.dirtyColMin[row] = 0u; + gRender.dirtyColMax[row] = ST_SCREEN_COLS - 1u; + } return; } - *x0 = SURFACE_WIDTH; - *y0 = SURFACE_HEIGHT; - *x1 = 0; - *y1 = 0; + // 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 by = cellRow(cell); - int16_t cy = (int16_t)(by * 8u); - int16_t cx = (int16_t)((cell - ((uint16_t)by << 5) - ((uint16_t)by << 3)) * 8u); - if (cx < *x0) { - *x0 = cx; + 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 (cy < *y0) { - *y0 = cy; - } - if ((int16_t)(cx + 8) > *x1) { - *x1 = (int16_t)(cx + 8); - } - if ((int16_t)(cy + 8) > *y1) { - *y1 = (int16_t)(cy + 8); + 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; @@ -297,32 +400,88 @@ static void dropCache(void) { } -// The mono tile for character `chr`, rebuilt when the glyph's bits no -// longer match the cached shape (direct-mapped by character). -static const jlTileT *glyphTile(const StSimT *sim, uint8_t chr) { - // Two glyph bits -> one chunky byte of non-zero (set) / zero pixels. - static const uint8_t kMonoPair[4] = { 0x00u, 0x0Fu, 0xF0u, 0xFFu }; - const uint8_t *bits = sim->charset[chr]; - uint8_t slot = (uint8_t)(chr & (ST_GLYPH_CACHE - 1u)); - jlTileT *tile = &gRender.glyphTiles[slot]; - uint8_t chunky[TILE_BYTES]; - uint8_t row; +// 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; - if (gRender.glyphUsed[slot] && memcmp(gRender.glyphBits[slot], bits, 8u) == 0) { - return tile; - } + memset(slotOfMask, 0, sizeof(slotOfMask)); + gRender.logoSlotCount = 0u; for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { - uint8_t b = bits[row]; - uint8_t *p = &chunky[row * TILE_BYTES_PER_ROW]; - p[0] = kMonoPair[b >> 6]; - p[1] = kMonoPair[(b >> 4) & 3u]; - p[2] = kMonoPair[(b >> 2) & 3u]; - p[3] = kMonoPair[b & 3u]; + 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(tile, chunky); - memcpy(gRender.glyphBits[slot], bits, 8u); - gRender.glyphUsed[slot] = true; - return tile; + 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]; } @@ -360,8 +519,9 @@ static void paintCells(jlSurfaceT *stage, StSimT *sim) { } -// Paint one cell: its glyph tile coloured from colour RAM over the -// background, and note the cell in the character's range. +// 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]; @@ -371,7 +531,14 @@ static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8 if (cell > gRender.glyphLast[chr]) { gRender.glyphLast[chr] = cell; } - jlTilePasteMono(stage, bx, by, glyphTile(sim, chr), sim->color[cell], sim->bgColor); + // 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); } @@ -407,18 +574,32 @@ static void tileFromChunky(jlTileT *out, const uint8_t *chunky) { // --------------------------------------------------------------------------- void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { - int16_t dx0; - int16_t dy0; - int16_t dx1; - int16_t dy1; 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); - dirtyRect(sim, &dx0, &dy0, &dx1, &dy1); + 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]; @@ -428,12 +609,10 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { 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 == sim->frame.color[idx] && d->multi == multi); + 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 && dx1 > dx0) { - if (px < dx1 && (int16_t)(px + ST_SPRITE_W) > dx0 && py < dy1 && (int16_t)(py + ST_SPRITE_PX) > dy0) { - same = false; - } + if (same && want && dirtyUnderSprite(px, py)) { + same = false; } if (!same) { first = k; @@ -471,7 +650,7 @@ void stRenderFrame(jlSurfaceT *stage, StSimT *sim) { d->x = sim->frame.x[idx]; d->y = sim->frame.y[idx]; d->ptr = sim->frame.ptr[idx]; - d->color = sim->frame.color[idx]; + d->color = drawColor(sim, idx); d->multi = (uint8_t)((sim->frame.multiMask >> idx) & 1u); } { @@ -492,6 +671,7 @@ void stRenderInit(jlSurfaceT *stage) { 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]; } @@ -551,6 +731,24 @@ static bool loadPrecompiledCels(void) { } + +// "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 }; @@ -611,11 +809,36 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) { 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); - memset(sim->charDirty, 1, ST_CHARSET_CHARS); + 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); } diff --git a/stSim.c b/stSim.c index 289e887..af93ba1 100644 --- a/stSim.c +++ b/stSim.c @@ -22,6 +22,7 @@ static const uint8_t kPumpFreq[5] = { 0x04, 0x05, 0x07, 0x0A, 0x10 }; static void applyVelocityX(StSimT *sim); static void applyVelocityY(StSimT *sim); static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py); +static int16_t cellOfPixel(int16_t px); static void bitScrollHatch(StSimT *sim); static void collisionDispatch(StSimT *sim); static void collisionPhase1(StSimT *sim); @@ -99,8 +100,8 @@ static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py) { return 0u; } // Floor division so a negative px lands on the column to its left. - firstCol = (int16_t)((px - (px < 0 ? 7 : 0)) / 8); - shift = (uint8_t)(px - firstCol * 8); + firstCol = cellOfPixel(px); + shift = (uint8_t)(px - (int16_t)(firstCol << 3)); for (k = 0u; k < 4u; k++) { uint8_t byte = 0u; col = (int16_t)(firstCol + (int16_t)k); @@ -113,6 +114,17 @@ static uint32_t backgroundRowMask(const StSimT *sim, int16_t px, int16_t py) { } +// Which character column (or row) a pixel coordinate falls in: floor +// division by 8. A shift, not a divide -- the 65816 has no divider and +// the compiler's signed-division helper cost more than the collision +// test that calls it. The +256/-32 bias makes the shift exact for any +// coordinate down to -256, well past the furthest a sprite can hang off +// the left or top edge. +static int16_t cellOfPixel(int16_t px) { + return (int16_t)(((px + 256) >> 3) - 32); +} + + // $63D0 -- rotate every row of the transporter hatch glyph one pixel // right: the animated energy field in the top-wall opening. static void bitScrollHatch(StSimT *sim) { @@ -122,7 +134,7 @@ static void bitScrollHatch(StSimT *sim) { uint8_t b = sim->charset[ST_CHAR_TRANSPORTER][row]; sim->charset[ST_CHAR_TRANSPORTER][row] = (uint8_t)((b >> 1) | (b << 7)); } - sim->charDirty[ST_CHAR_TRANSPORTER] = 1u; + stSimMarkChar(sim, ST_CHAR_TRANSPORTER); } @@ -264,6 +276,7 @@ static StTickResultE collisionPhase3(StSimT *sim) { } + // The C64 reads two VIC collision registers each frame: $D01F (a sprite // touched the background) and $D01E (two sprites touched). Only the cab // (sprite 0) can drive a gameplay outcome from them: the crash dispatch @@ -298,9 +311,9 @@ static void computeCollisions(StSimT *sim) { // any non-space cell falls through to the exact test, so the result // is unchanged (space is the only zero-pixel glyph the test can hit). { - int16_t firstCol = (int16_t)((cabX - (cabX < 0 ? 7 : 0)) / 8); - int16_t topRow = (cabY < 0) ? 0 : (int16_t)(cabY / 8); - int16_t botRow = (int16_t)((cabY + (int16_t)ST_SPRITE_H - 1) / 8); + int16_t firstCol = cellOfPixel(cabX); + int16_t topRow = (cabY < 0) ? 0 : cellOfPixel(cabY); + int16_t botRow = cellOfPixel((int16_t)(cabY + (int16_t)ST_SPRITE_H - 1)); bool solid = false; int16_t rr; int16_t cc; @@ -928,6 +941,8 @@ static void taxiSpriteCelSelect(StSimT *sim) { // $4345 -- a HUD glyph's numeric value: blanks and the zero glyph are 0. + + static uint8_t validateDigit(uint8_t ch) { uint8_t v; @@ -1073,7 +1088,8 @@ void stSimEnterLevel(StSimT *sim, const StLevelT *level) { memcpy(sim->color, level->color, ST_SCREEN_CELLS); stSimDirtyAll(sim); memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); - memset(sim->charDirty, 1, ST_CHARSET_CHARS); + sim->charDirtyAll = true; + sim->charDirtyCount = 0u; // Level sprites live in the one level buffer, so the same bitmap // address now holds other pixels: rebuild every collision mask. memset(sim->collMaskBm, 0, sizeof(sim->collMaskBm)); @@ -1147,9 +1163,14 @@ void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { uint8_t keepWalk = sim->walkParity; // $5EC4 clears the game state but the playback buffer, the flame - // parity, the wave index and the walk parity all survive. + // parity, the wave index and the walk parity all survive. So does + // the displayed picture -- screen RAM, colour RAM and the charset + // are not game state, and the attract demo's "GET READY" banner is + // drawn over the still-live title screen -- so the clear starts at + // ST_SIM_CLEAR_FROM. Every level entry rebinds the image anyway + // (stSimEnterLevel), so nothing downstream sees the difference. memcpy(keepBuf, sim->demoBuf, sizeof(keepBuf)); - memset(sim, 0, sizeof(*sim)); + memset((uint8_t *)sim + ST_SIM_CLEAR_FROM, 0, sizeof(*sim) - ST_SIM_CLEAR_FROM); memcpy(sim->demoBuf, keepBuf, sizeof(keepBuf)); sim->flameParity = keepParity; sim->waveIdx = keepWave; @@ -1173,6 +1194,30 @@ void stSimNewGame(StSimT *sim, uint8_t playerCount, bool demo) { // Everything must be repainted. +// A glyph's bits changed: the renderer has to repaint every cell showing +// that character. The list is short (one or two per frame), so a linear +// scan for duplicates is cheaper than any index; an overflow just says +// "all of them" and costs one full repaint. +void stSimMarkChar(StSimT *sim, uint8_t ch) { + uint8_t k; + + if (sim->charDirtyAll) { + return; + } + for (k = 0u; k < sim->charDirtyCount; k++) { + if (sim->charDirtyList[k] == ch) { + return; + } + } + if (sim->charDirtyCount == ST_CHAR_DIRTY_MAX) { + sim->charDirtyAll = true; + return; + } + sim->charDirtyList[sim->charDirtyCount] = ch; + sim->charDirtyCount++; +} + + void stSimDirtyAll(StSimT *sim) { memset(sim->cellDirty, 1, ST_SCREEN_CELLS); sim->dirtyCount = 0u; diff --git a/stSim.h b/stSim.h index 6701b85..718a7df 100644 --- a/stSim.h +++ b/stSim.h @@ -20,6 +20,7 @@ #define ST_SIM_H #include +#include #include #include @@ -30,7 +31,11 @@ #define ST_SCREEN_COLS 40u #define ST_SCREEN_ROWS 25u #define ST_SCREEN_CELLS (ST_SCREEN_COLS * ST_SCREEN_ROWS) -#define ST_CELL(row, col) ((uint16_t)((row) * ST_SCREEN_COLS + (col))) +// row * 40 + col. Written as shifts (40 == 32 + 8) because the 65816 has +// no multiply: with a variable row the * form called the compiler's +// software multiply helper, and the collision pre-check does that a +// dozen times a tick. Constant rows still fold to a constant. +#define ST_CELL(row, col) ((uint16_t)((((uint16_t)(row)) << 5) + (((uint16_t)(row)) << 3) + (uint16_t)(col))) // VIC sprite coordinate origins: the visible picture starts at // sprite-X 24 and sprite-Y 50. @@ -68,6 +73,22 @@ #define ST_CHAR_CAB_ICON 0xC8u #define ST_CHAR_MENU_SENTINEL 0x7Bu // $07A6 == $7B: first fuel cell half = out of fuel +// Title logo ($4827): the logo is one character repeated over rows +// 1..11, and the flip-book animates it by copying one of four frame +// glyphs over it. The glyph stays the simulation's own record of which +// frame is showing; the renderer draws all four frames as one tile in +// reserved palette slots and animates them by rewriting those entries, +// so a flip and a colour cycle both cost no repaint at all. +#define ST_LOGO_CHAR 0x84u +#define ST_LOGO_FRAME_FIRST 0x85u // $85..$88 = frames 0..3 +#define ST_LOGO_FRAMES 4u +// Offset stSimNewGame clears from: the block above it is the displayed +// picture and survives a new game (see StSimT). +#define ST_SIM_CLEAR_FROM offsetof(StSimT, level) +// Distinct characters whose glyph can change in one frame before the +// renderer gives up and repaints everything. +#define ST_CHAR_DIRTY_MAX 8u + // Level image as loaded from a .dat (STL4). Kept in the C64's units. #define ST_MAX_PADS 10u #define ST_MAX_LEVEL_SPRITES 16u @@ -166,9 +187,13 @@ typedef enum { } StTickResultE; typedef struct { - // ---- level image (mutable copies: hooks edit the pad table) ---- - const StLevelT *level; - StPadT pads[ST_MAX_PADS]; // $7D0A working copy + // ---- the displayed picture: screen RAM, colour RAM, the charset + // and the renderer's dirty bookkeeping for them. stSimNewGame + // ($5EC4) does NOT clear this block -- the C64 resets its game + // variables with the previous picture still on screen, which is how + // the attract demo's "GET READY" banner comes up over the live + // title. Everything from `level` down IS cleared; keep new fields on + // the right side of that line ---- uint8_t screen[ST_SCREEN_CELLS]; // $0400 screen RAM uint8_t color[ST_SCREEN_CELLS]; // $D800 colour RAM uint8_t cellDirty[ST_SCREEN_CELLS]; // renderer clears @@ -176,9 +201,26 @@ typedef struct { uint16_t dirtyCount; bool dirtyAll; // list overflowed / whole screen uint8_t charset[ST_CHARSET_CHARS][8]; // $2800 working copy - uint8_t charDirty[ST_CHARSET_CHARS]; // renderer clears + // Characters whose glyph bits the game has edited since the last + // frame -- the transporter hatch, the title logo flip, the laser + // beams. A short list, not a 256-entry flag array: the renderer had + // to scan all 256 every single frame to find the one or two that + // ever change. Overflow (or a scene load) sets charDirtyAll. + uint8_t charDirtyList[ST_CHAR_DIRTY_MAX]; + uint8_t charDirtyCount; + bool charDirtyAll; uint8_t borderColor; // $D020 uint8_t bgColor; // $D021 + // The colour the logo band's cells would carry ($4861 writes it into + // 407 cells of colour RAM); part of the picture, so it survives the + // same way colour RAM does. + uint8_t logoColor; + + // ---- cleared by stSimNewGame from here down ---- + + // ---- level image (mutable copies: hooks edit the pad table) ---- + const StLevelT *level; + StPadT pads[ST_MAX_PADS]; // $7D0A working copy // ---- input ---- uint8_t inputMask; // $7169 EOR $FF of $DC00: 1 = pressed @@ -315,6 +357,8 @@ bool stSimAdvancePlayer(StSimT *sim); void stSimArchiveHud(StSimT *sim); // Mark every cell dirty (scene change). void stSimDirtyAll(StSimT *sim); +// A glyph's bits changed: every cell showing it has to be repainted. +void stSimMarkChar(StSimT *sim, uint8_t ch); // Text and HUD primitives (screen RAM writes). void stSimDrawText(StSimT *sim, uint8_t col, uint8_t row, const uint8_t *text, uint8_t color); void stSimPutChar(StSimT *sim, uint8_t col, uint8_t row, uint8_t ch); diff --git a/stTitle.c b/stTitle.c index 02890e7..8536963 100644 --- a/stTitle.c +++ b/stTitle.c @@ -31,7 +31,6 @@ static const int8_t kIntroCabDy[4] = { -2, 1, -1, 1 }; static const uint8_t kTextDemoExit[] = { 0x44, 0x45, 0x4D, 0x4F, 0x2C, 0x20, 0x55, 0x53, 0x45, 0x20, 0x4A, 0x4F, 0x59, 0x53, 0x54, 0x49, 0x43, 0x4B, 0x20, 0x54, 0x4F, 0x20, 0x45, 0x58, 0x49, 0x54, 0 }; static const uint8_t kTextScreens[] = { 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x31, 0x20, 0x4F, 0x46, 0x20, 0x32, 0x35, 0x20, 0x44, 0x49, 0x46, 0x46, 0x45, 0x52, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x43, 0x52, 0x45, 0x45, 0x4E, 0x53, 0x21, 0 }; -#define ST_LOGO_CHAR 0x84u #define ST_TITLE_CAB_LIFT_ROW 0x14u #define ST_TITLE_SPARKLE_TICKS 0x5Au #define ST_TITLE_HOVER_COL 0x28u @@ -65,21 +64,19 @@ static void addToSpriteX(StSimT *sim, uint8_t idx, uint8_t delta) { } -// $4861 -- next logo colour over rows 1..11, columns 1..37, and onto -// sprites 3..7. +// $4861 -- the next logo colour, onto sprites 3..7 and onto the logo +// itself. The C64 writes the colour into all 407 cells of rows 1..11, +// columns 1..37; only the cells showing the logo character take any +// colour from it (the rest are blank), so the colour is published in +// logoColor instead and the renderer applies it to the logo's palette +// slot. Nothing reads the band's colour RAM. static void logoColorCycle(StSimT *sim) { uint8_t color; - uint8_t row; - uint8_t col; uint8_t k; sim->logoColorIdx = (uint8_t)((sim->logoColorIdx + 1u) & 7u); color = kLogoColor[sim->logoColorIdx]; - for (row = 1u; row <= 11u; row++) { - for (col = 1u; col <= 37u; col++) { - stSimPutColor(sim, col, row, color); - } - } + sim->logoColor = color; for (k = 3u; k < ST_HW_SPRITES; k++) { sim->spr[k].color = color; } @@ -94,7 +91,7 @@ static void logoFlip(StSimT *sim) { return; } memcpy(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_CHAR + kLogoFlip[sim->logoCycleAux]], 8u); - sim->charDirty[ST_LOGO_CHAR] = 1u; + stSimMarkChar(sim, ST_LOGO_CHAR); sim->logoCycleAux++; if (sim->logoCycleAux == 6u) { sim->logoCycleAux = 0u; @@ -174,7 +171,8 @@ void stIntroEnter(StSimT *sim, const StLevelT *level) { memset(sim->color, 0, ST_SCREEN_CELLS); stSimDirtyAll(sim); memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); - memset(sim->charDirty, 1, ST_CHARSET_CHARS); + sim->charDirtyAll = true; + sim->charDirtyCount = 0u; stSimDrawText(sim, 10u, 12u, level->name, 3u); if (sim->demoMode != 0u && sim->postMortem == 0u) { stSimDrawText(sim, 7u, 3u, kTextDemoExit, 5u); @@ -240,7 +238,8 @@ void stTitleEnter(StSimT *sim, const StLevelT *title) { memcpy(sim->color, title->color, ST_SCREEN_CELLS); stSimDirtyAll(sim); memcpy(sim->charset, stC64Charset(), sizeof(sim->charset)); - memset(sim->charDirty, 1, ST_CHARSET_CHARS); + sim->charDirtyAll = true; + sim->charDirtyCount = 0u; logoColorCycle(sim); for (k = 0u; k < ST_HW_SPRITES; k++) { sim->spr[k].enable = 0u;