From 2acfce011dffdad3ff5c1fd803e6df9e28ca8e21 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 17 Sep 2026 17:46:47 -0500 Subject: [PATCH] Space Taxi work. --- examples/spacetaxi/assets/font.png | Bin 1379 -> 129 bytes examples/spacetaxi/assets/tiles/tbank0.png | Bin 1879 -> 129 bytes examples/spacetaxi/assets/tiles/tbank1.png | Bin 1201 -> 129 bytes examples/spacetaxi/assets/tiles/tbank2.png | Bin 1237 -> 129 bytes examples/spacetaxi/assets/tiles/tilebank0.png | Bin 1039 -> 129 bytes examples/spacetaxi/spacetaxi.c | 15 +- examples/spacetaxi/spacetaxi.h | 12 +- examples/spacetaxi/stHooks.c | 11 +- examples/spacetaxi/stRender.c | 413 +++++++++--- examples/spacetaxi/stSim.c | 63 +- examples/spacetaxi/stSim.h | 54 +- examples/spacetaxi/stTitle.c | 25 +- include/joey/platform.h | 4 + include/joey/tile.h | 17 + make/x68000.mk | 12 +- scripts/bench-dos.sh | 8 +- scripts/run-iigs.sh | 19 +- scripts/run-x68000.sh | 87 +++ scripts/verify-x68000-golden.sh | 26 +- src/amiga/amigaTile.h | 46 +- src/atarist/stTile.h | 53 +- src/core/port.h | 24 + src/core/sprite.c | 2 + src/core/surface.c | 28 + src/core/surfaceInternal.h | 34 + src/core/tile.c | 27 + src/generic/genericTile.c | 29 + src/iigs/hal.c | 14 + src/iigs/joeyDraw.s | 615 +++++++++++++++++- src/x68000/x68kTile.h | 46 +- tools/scanPtrBug.py | 48 ++ tools/xdftool.py | 198 +++++- 32 files changed, 1729 insertions(+), 201 deletions(-) create mode 100755 scripts/run-x68000.sh create mode 100755 tools/scanPtrBug.py diff --git a/examples/spacetaxi/assets/font.png b/examples/spacetaxi/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/examples/spacetaxi/assets/tiles/tbank0.png b/examples/spacetaxi/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/examples/spacetaxi/assets/tiles/tbank1.png b/examples/spacetaxi/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/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c index 6a1f104..65fce0e 100644 --- a/examples/spacetaxi/stRender.c +++ b/examples/spacetaxi/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/examples/spacetaxi/stSim.c b/examples/spacetaxi/stSim.c index 289e887..af93ba1 100644 --- a/examples/spacetaxi/stSim.c +++ b/examples/spacetaxi/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/examples/spacetaxi/stSim.h b/examples/spacetaxi/stSim.h index 6701b85..718a7df 100644 --- a/examples/spacetaxi/stSim.h +++ b/examples/spacetaxi/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/examples/spacetaxi/stTitle.c b/examples/spacetaxi/stTitle.c index 02890e7..8536963 100644 --- a/examples/spacetaxi/stTitle.c +++ b/examples/spacetaxi/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; diff --git a/include/joey/platform.h b/include/joey/platform.h index eadd446..f4b6de5 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -120,6 +120,7 @@ #define JL_HAS_TILE_COPY_MASKED // iigsTileCopyMaskedInner, macro #define JL_HAS_TILE_PASTE // iigsTilePasteInner, macro #define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else + #define JL_HAS_TILE_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else #define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: inline word copy for // short spans (llvm-mos memcpy is a slow far @@ -188,6 +189,7 @@ #define JL_HAS_TILE_PASTE // amiga planar tile paste, inline (amigaTile.h) #define JL_HAS_TILE_SNAP // amiga planar tile snap, inline (amigaTile.h) #define JL_HAS_TILE_PASTE_MONO // amiga planar mono colorize+paste, inline (amigaTile.h) + #define JL_HAS_TILE_PASTE_GLYPH // amiga 1bpp glyph -> planes, inline (amigaTile.h) #define JL_HAS_SPRITE_DRAW // amiga planar sprite draw, function #define JL_HAS_SPRITE_SAVE // amiga planar sprite save, function #define JL_HAS_SPRITE_RESTORE // amiga planar sprite restore, function @@ -256,6 +258,7 @@ #define JL_HAS_TILE_PASTE // st planar tile paste, inline (stTile.h) #define JL_HAS_TILE_SNAP // st planar tile snap, inline (stTile.h) #define JL_HAS_TILE_PASTE_MONO // st planar mono colorize+paste, inline (stTile.h) + #define JL_HAS_TILE_PASTE_GLYPH // st 1bpp glyph -> planes, inline (stTile.h) #define JL_HAS_SPRITE_DRAW // st planar sprite draw, function #define JL_HAS_SPRITE_SAVE // st planar sprite save, function #define JL_HAS_SPRITE_RESTORE // st planar sprite restore, function @@ -378,6 +381,7 @@ #define JL_HAS_TILE_COPY_MASKED #define JL_HAS_TILE_PASTE #define JL_HAS_TILE_SNAP + #define JL_HAS_TILE_PASTE_GLYPH // x68k 1bpp glyph -> planes, inline (x68kTile.h) #define JL_HAS_TILE_PASTE_MONO #define JL_HAS_SPRITE_DRAW #define JL_HAS_SPRITE_SAVE diff --git a/include/joey/tile.h b/include/joey/tile.h index 871f73f..6a88931 100644 --- a/include/joey/tile.h +++ b/include/joey/tile.h @@ -116,6 +116,23 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in); void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor); +// Paste an 8x8 ONE-BIT-PER-PIXEL glyph at block (bx, by): bits[row] is +// that row of the glyph, most significant bit leftmost; a set bit draws +// fgColor and a clear bit draws bgColor. Both colors are masked to 4 +// bits. Opaque. +// +// This is the shape a character generator already has -- a font ROM, a +// C64 charset, anything 1bpp -- so nothing has to expand it into a +// jlTileT first. Prefer it over jlTilePasteMono for glyphs: a jlTileT +// is target-native (plane-major on the planar ports), whereas the +// silhouette a mono paste wants is chunky, so feeding one a tile that +// came from jlTileSnap is a format mismatch that draws the wrong shape. +// Taking the bits directly makes the input unambiguous, and lets each +// port expand them once instead of twice. +void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, + const uint8_t *bits, uint8_t fgColor, uint8_t bgColor); + + // Draw a NUL-terminated ASCII string at block (bx, by) using glyphs // pulled from fontSurface. // diff --git a/make/x68000.mk b/make/x68000.mk index 4e6ba26..3f9cbb8 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -95,6 +95,7 @@ SERIAL_SRC := $(EXAMPLES)/serial/serial.c UBER_SRC := $(EXAMPLES)/uber/uber.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c KEYS_SRC := $(EXAMPLES)/keys/keys.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c .PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean @@ -102,7 +103,7 @@ all x68000: x68000-lib x68000-examples x68000-lib: $(LIB) $(LIBXMP_AR) -x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X $(BINDIR)/KEYS.X +x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X $(BINDIR)/KEYS.X $(BINDIR)/STAXI.X $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -166,6 +167,15 @@ $(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ +# Space Taxi. -s (strip) because the image it ships on is a 1232 KB +# Human68k floppy: the unstripped binary is most of the volume on its +# own. No precompiled sprite bank is staged -- this port has a real 68k +# planar emitter, so the boot prewarm JITs its cels in well under a +# second (the reason the IIgs needs a baked .spc is its ~0.4 s per cel). +$(BINDIR)/STAXI.X: $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -s -I$(EXAMPLES)/spacetaxi $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ + # Both-directions RS-232C gate (~1 min). x68000-verify-serial: $(BINDIR)/SERIAL.X $(REPO_DIR)/scripts/verify-x68000-serial.sh diff --git a/scripts/bench-dos.sh b/scripts/bench-dos.sh index 779602b..21a8b6a 100755 --- a/scripts/bench-dos.sh +++ b/scripts/bench-dos.sh @@ -39,11 +39,17 @@ cleanup() { trap cleanup EXIT echo "bench-dos: running UBER headless (timeout ${timeout_s}s)..." >&2 +# Mount with an explicit -c, NOT the positional path: DOSBox runs every +# -c line BEFORE it auto-mounts the trailing directory, so "C:" and +# "UBER.EXE" ran against a drive that did not exist yet ("Drive C does +# not exist!" / "Illegal command") and the run sat at Z:\> until the +# timeout with no JOEYLOG.TXT. DISPLAY=$display dosbox \ -conf "$conf" \ + -c "MOUNT C \"$bin_dir\"" \ -c "C:" \ -c "UBER.EXE" \ - "$bin_dir" >/tmp/bench-dos-dosbox.log 2>&1 & + >/tmp/bench-dos-dosbox.log 2>&1 & dosbox_pid=$! waited=0 diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index 6c06392..ea27d0e 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -32,6 +32,19 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) bin_dir=$repo/build/iigs/bin sys_disk=$repo/toolchains/emulators/support/gsos-system.po +# MAME: an installed one wins, otherwise the apple2gs build this repo +# already carries in toolchains/cache. Resolving it here means a shell +# whose PATH predates ~/.local/bin (a symlink created after login) does +# not fail with a bare "mame: command not found" three hundred lines in. +MAME="${MAME:-$(command -v mame || true)}" +if [[ -z $MAME ]]; then + MAME=$repo/toolchains/cache/mame-mame0264/apple2gs +fi +if [[ ! -x $MAME ]]; then + echo "run-iigs: no MAME found (set MAME=/path/to/mame, or apt install mame)" >&2 + exit 2 +fi + # --all boots the full example set off a CFFA2 hard-disk image; an # optional example name after it auto-launches that one. all_mode=0 @@ -195,7 +208,7 @@ LUA cd "$work" # -ramsize 4M: AGI (on this disk) needs the larger Memory Manager pool; # harmless for the smaller demos. - mame apple2gs \ + "$MAME" apple2gs \ -ramsize 4M \ -rompath "$rompath" \ -sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/joey-all.2mg" \ @@ -354,7 +367,7 @@ EOF # -ramsize 4M: AGI's ~240KB app plus GS/OS/Finder leaves the stock # 2MB Memory Manager pool too tight -- its first jlAlloc (the view # cache) returns NULL and the app exits. 4MB is the assumed target. - mame apple2gs \ + "$MAME" apple2gs \ -ramsize 4M \ -rompath "$rompath" \ -sl7 cffa2 -hard1 "$work/boot.po" -hard2 "$work/agi.2mg" \ @@ -517,7 +530,7 @@ EOF # -snapshot_directory is explicit because MAME otherwise resolves # it against the user's mame.ini (snapshots vanished into # ~/.mame/snap during bring-up). - mame apple2gs \ + "$MAME" apple2gs \ -rompath "$rompath" \ -flop3 "$work/boot.po" \ -flop4 "$work/joey-run.2mg" \ diff --git a/scripts/run-x68000.sh b/scripts/run-x68000.sh new file mode 100755 index 0000000..920a59e --- /dev/null +++ b/scripts/run-x68000.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# run-x68000.sh - launch a built X68000 example under MAME. +# +# scripts/run-x68000.sh staxi Space Taxi (binary + its DATA tree) +# scripts/run-x68000.sh uber any other built example, by name +# X68K_HEADLESS=1 scripts/run-x68000.sh staxi windowless + snapshots +# +# TWO FLOPPIES, the same split verify-x68000-golden.sh uses: Human68k plus +# AUTOEXEC on A:, and the program with its assets on B:. A 1232 KB floppy +# cannot hold Human68k and a ~285 KB .X together, and AUTOEXEC switches to +# B: first so the program's CWD is the disk its DATA/ tree is on. +# +# Needs toolchains/x68mame (HUMAN302.XDF + roms/) -- see the X68000 memory +# note for where to copy it from. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +prog=${1:-staxi} +PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]') +bin=$repo/build/x68000/bin/$PROG.X +x68=${X68K_DIR:-$repo/toolchains/x68mame} +template=$x68/HUMAN302.XDF + +# MAME: the x68k build this repo carries, unless one is installed or named. +MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}" +if [[ ! -x $MAME ]]; then + MAME="$(command -v mame || true)" +fi +[[ -n ${MAME:-} && -x $MAME ]] || { echo "run-x68000: no MAME (set MAME=...)" >&2; exit 2; } +[[ -f $bin ]] || { echo "run-x68000: $bin not built (make -f make/x68000.mk)" >&2; exit 2; } +[[ -f $template ]] || { echo "run-x68000: missing $template (copy toolchains/x68mame in)" >&2; exit 2; } + +work=$(mktemp -d -t joey-x68run.XXXXXX) +trap 'rm -rf "$work"' EXIT + +printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT" +cp "$template" "$work/boot.xdf" +cp "$template" "$work/data.xdf" +# Data disk = the template emptied (keeps the BPB and boot sector intact). +# `empty` and not a list of filenames: the stock disk carries ~900 KB of +# Human68k utilities in SUBDIRECTORIES (SYS, BIN, BASIC2, ...), and deleting +# only the root files leaves every one of those clusters allocated -- which is +# what made a 1232 KB floppy look like it had 330 KB to give. +python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null +# Boot disk keeps only what it takes to boot and launch one .X. +for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do + python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true +done +for d in SYS HIS BIN BASIC2 ASK ETC; do + python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true +done +python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null +python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null + +# The example's DATA tree, if it has one, at the same paths jlDataOpen builds +# ("DATA/" + the app's relative name). Human68k names are 8.3 upper case. +data_dir=$repo/build/x68000/bin/DATA +if [[ -d $data_dir ]]; then + while IFS= read -r f; do + rel=${f#"$data_dir"/} + python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \ + "DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null + done < <(find "$data_dir" -type f | sort) +fi +echo "run-x68000: $(python3 "$repo/tools/xdftool.py" free "$work/data.xdf")" + +if [[ ${X68K_HEADLESS:-0} == 1 ]]; then + snap=${X68K_SNAP:-/tmp/run-x68000-snap} + rm -rf "$snap"; mkdir -p "$snap" + cat > "$work/snap.lua" <<'LUA' +local n = 0 +emu.register_frame_done(function() + n = n + 1 + if n % 1200 == 0 then manager.machine.video:snapshot() end + if n >= 24000 then manager.machine:exit() end +end) +LUA + "$MAME" x68000 -bios ipl10 -rompath "$x68/roms" \ + -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \ + -video none -sound none -nothrottle \ + -snapshot_directory "$snap" -autoboot_script "$work/snap.lua" /dev/null 2>&1 || true + echo "run-x68000: snapshots -> $snap" +else + echo "run-x68000: booting Human68k, then B:\\$PROG.X. Quit MAME to end." + "$MAME" x68000 -bios ipl10 -rompath "$x68/roms" \ + -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" -skip_gameinfo +fi diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh index 6e8238c..6456baa 100755 --- a/scripts/verify-x68000-golden.sh +++ b/scripts/verify-x68000-golden.sh @@ -30,8 +30,11 @@ export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH" work=$(mktemp -d -t joey-x68gold.XXXXXX) trap 'rm -rf "$work"' EXIT -# -s (strip) is REQUIRED: the unstripped binary is ~247 KB and does not fit -# alongside Human68k on a 1232 KB floppy (232 KB free). +# -s (strip) keeps the staged binary small. (It used to be load-bearing: the +# data disk was built by deleting the template's root FILES, which leaves the +# stock ~900 KB of Human68k utilities in SYS/ BIN/ BASIC2/ ... still allocated +# and only ~330 KB usable. `xdftool empty` frees the whole 1221-cluster disk, +# so the size pressure is gone -- the strip is now just tidiness.) m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ -DJOEYLIB_PLATFORM_X68000 -DUBER_FRAMES=1u \ -I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \ @@ -40,21 +43,24 @@ m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ "$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \ -o "$work/UBER.X" || exit 1 -# TWO DISKS. UBER.X is ~250 KB and Human68k needs ~90 KB, which no longer fit -# together on a 1232 KB floppy at all -- so the boot disk carries only Human68k -# and AUTOEXEC, while the binary AND joeylog.txt live on the second disk on B:. -# AUTOEXEC switches to B: before launching so the log lands there too. +# TWO DISKS: the boot disk carries only Human68k and AUTOEXEC, while the binary +# AND joeylog.txt live on the second disk on B:. AUTOEXEC switches to B: before +# launching so the log lands there too. Keeping the writable log off the boot +# disk also means a run can never corrupt the thing that boots it. printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" cp "$TEMPLATE" "$work/gold.xdf" -# Data disk = the template with every file removed (keeps the format). +# Data disk = the template emptied (keeps the BPB and boot sector). `empty` +# rather than a list of filenames: the stock disk's utilities live in +# SUBDIRECTORIES, which `delete` cannot see or free. cp "$TEMPLATE" "$work/data.xdf" -for f in HUMAN.SYS CONFIG.SYS KEY.SYS USKCG.SYS BEEP.SYS STARTUP.ENV COMMAND.X AUTOEXEC.BAT; do - python3 "$repo/tools/xdftool.py" delete "$work/data.xdf" "$f" >/dev/null 2>&1 -done +python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null # Trim the boot disk of anything not needed to boot and run one .X. for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do python3 "$repo/tools/xdftool.py" delete "$work/gold.xdf" "$f" >/dev/null 2>&1 done +for d in SYS HIS BIN BASIC2 ASK ETC; do + python3 "$repo/tools/xdftool.py" deltree "$work/gold.xdf" "$d" >/dev/null 2>&1 +done python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 python3 "$repo/tools/xdftool.py" add "$work/gold.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null diff --git a/src/amiga/amigaTile.h b/src/amiga/amigaTile.h index bb00e6c..333b508 100644 --- a/src/amiga/amigaTile.h +++ b/src/amiga/amigaTile.h @@ -305,13 +305,32 @@ static inline __attribute__((always_inline)) void amigaTileMapPaste(jlSurfaceT * // writes // outPlaneK = (shape & maskFgK) | (~shape & maskBgK) // where maskXK = $FF if (X & (1 << k)) else $00. +// Eight 1bpp shape rows into the bitplanes: set bits take the +// foreground's plane bit, clear bits the background's. Shared by the +// mono paste (which folds a chunky silhouette into these rows first) +// and the glyph paste (whose input already IS these rows). +static inline __attribute__((always_inline)) void amigaTilePasteShape(AmigaPlanarT *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { + uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; + uint8_t plane; + uint8_t row; + + for (plane = 0; plane < AMIGA_BITPLANES; plane++) { + uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t *p = pd->planes[plane] + rowBase; + for (row = 0; row < 8u; row++) { + uint8_t s = shape[row]; + p[row * AMIGA_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); + } + } +} + + static inline __attribute__((always_inline)) void amigaTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { AmigaPlanarT *pd; uint8_t shape[TILE_PIXELS_PER_SIDE]; - uint8_t plane; uint8_t row; uint8_t col; - uint16_t rowBase; pd = (AmigaPlanarT *)dst->portData; if (pd == NULL) { @@ -333,16 +352,21 @@ static inline __attribute__((always_inline)) void amigaTilePasteMono(jlSurfaceT } shape[row] = bits; } - rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t *p = pd->planes[plane] + rowBase; - for (row = 0; row < 8u; row++) { - uint8_t s = shape[row]; - p[row * AMIGA_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); - } + amigaTilePasteShape(pd, bx, by, shape, fgColor, bgColor); +} + + +// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape +// the mono paste has to fold out of a chunky silhouette, so this skips +// the fold entirely (and cannot be handed the wrong tile format). +static inline __attribute__((always_inline)) void amigaTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + AmigaPlanarT *pd = (AmigaPlanarT *)dst->portData; + + if (pd == NULL) { + jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); + return; } + amigaTilePasteShape(pd, bx, by, bits, fgColor, bgColor); } #endif diff --git a/src/atarist/stTile.h b/src/atarist/stTile.h index a1f3a8e..5af8e92 100644 --- a/src/atarist/stTile.h +++ b/src/atarist/stTile.h @@ -244,6 +244,17 @@ static inline __attribute__((always_inline)) void stTileMapPaste(jlSurfaceT *dst // at dstAddr + k*2 inside the 16-pixel group (4 plane words = 8 bytes // per group). bx odd selects the second byte of each plane word (the // low / right 8 pixels of the group). +// One 8-pixel row of a 1bpp shape into the four interleaved planes: +// set bits take the foreground's plane bit, clear bits the background's. +#define ST_MONO_ROW(_dst, _shape, _fg, _bg) do { \ + uint8_t shape_ = (uint8_t)(_shape); \ + (_dst)[0] = (uint8_t)((uint8_t)(shape_ & (_fg)[0]) | (uint8_t)((uint8_t)(~shape_) & (_bg)[0])); \ + (_dst)[2] = (uint8_t)((uint8_t)(shape_ & (_fg)[1]) | (uint8_t)((uint8_t)(~shape_) & (_bg)[1])); \ + (_dst)[4] = (uint8_t)((uint8_t)(shape_ & (_fg)[2]) | (uint8_t)((uint8_t)(~shape_) & (_bg)[2])); \ + (_dst)[6] = (uint8_t)((uint8_t)(shape_ & (_fg)[3]) | (uint8_t)((uint8_t)(~shape_) & (_bg)[3])); \ +} while (0) + + static inline __attribute__((always_inline)) void stTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { StPlanarT *pd; uint16_t group; @@ -282,14 +293,40 @@ static inline __attribute__((always_inline)) void stTilePasteMono(jlSurfaceT *ds shape = (uint8_t)(shape | 0x01u); } } - dstAddr[0] = (uint8_t)((uint8_t)(shape & masksFg[0]) | - (uint8_t)((uint8_t)(~shape) & masksBg[0])); - dstAddr[2] = (uint8_t)((uint8_t)(shape & masksFg[1]) | - (uint8_t)((uint8_t)(~shape) & masksBg[1])); - dstAddr[4] = (uint8_t)((uint8_t)(shape & masksFg[2]) | - (uint8_t)((uint8_t)(~shape) & masksBg[2])); - dstAddr[6] = (uint8_t)((uint8_t)(shape & masksFg[3]) | - (uint8_t)((uint8_t)(~shape) & masksBg[3])); + ST_MONO_ROW(dstAddr, shape, masksFg, masksBg); + dstAddr += ST_BYTES_PER_ROW; + } +} + + +// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape +// a mono paste has to fold out of a chunky silhouette, so this skips +// that fold entirely (and cannot be handed the wrong tile format). +static inline __attribute__((always_inline)) void stTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + StPlanarT *pd; + uint16_t group; + uint8_t *dstAddr; + uint8_t row; + uint8_t plane; + uint8_t masksFg[4]; + uint8_t masksBg[4]; + + if (dst->portData == NULL) { + jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); + return; + } + pd = (StPlanarT *)dst->portData; + for (plane = 0u; plane < 4u; plane++) { + masksFg[plane] = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); + masksBg[plane] = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); + } + group = (uint16_t)((uint16_t)bx >> 1); + dstAddr = pd->base + + (uint16_t)by * 8u * ST_BYTES_PER_ROW + + group * ST_BYTES_PER_GROUP + + (uint16_t)(bx & 1u); + for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { + ST_MONO_ROW(dstAddr, bits[row], masksFg, masksBg); dstAddr += ST_BYTES_PER_ROW; } } diff --git a/src/core/port.h b/src/core/port.h index b18a9b7..535a712 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -80,6 +80,7 @@ extern void iigsTilePasteInner (uint8_t *dstRow0, const uint8_t *srcTilePixels extern void iigsTilePasteMark (const uint8_t *srcTilePixels, uint16_t bx, uint16_t by); extern void iigsTileMapPasteMark (uint16_t bx, uint16_t by, uint16_t wTiles, uint16_t hTiles, const uint8_t *tiles, const uint8_t *map); extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); +extern void iigsTilePasteGlyphMark(const uint8_t *bits, uint16_t bx, uint16_t by, uint16_t fgColor, uint16_t bgColor); extern void iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0); extern void iigsCopyRectRows (uint8_t *dst, uint16_t srcLo, uint16_t srcBank, uint16_t rowBytes, uint16_t rows); extern void iigsQuitGS (void); @@ -215,6 +216,17 @@ extern jlSurfaceT *gStage; #define jlpTileSnap(_s, _bx, _by, _out) \ iigsTileSnapInner((_out), &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)]) +// tilePasteGlyph: the 1bpp-glyph paste with the dirty mark FUSED into the +// asm (one JSL, no separate iigsMarkDirtyRowsInner call). Stage-only like +// the other stage-gated macros; other surfaces defer to the portable +// generic. fg/bg arrive pre-masked to 4 bits. +#if defined(JL_HAS_TILE_PASTE_GLYPH) +#define jlpTilePasteGlyph(_d, _bx, _by, _b, _fg, _bg) \ + ((_d) == gStage \ + ? iigsTilePasteGlyphMark((_b), (uint16_t)(_bx), (uint16_t)(_by), (uint16_t)(_fg), (uint16_t)(_bg)) \ + : jlpGenericTilePasteGlyph((_d), (_bx), (_by), (_b), (_fg), (_bg))) +#endif + // tilePasteMono: fused mono colorize + paste in one asm pass (PERF-AUDIT #3) // -- no intermediate jlTileT and no second paste call. Stage-only like the // other stage-gated macros; non-stage surfaces defer to the portable-C @@ -371,6 +383,7 @@ extern uint16_t iigsGetTickWord(void); #define jlpTileCopyMasked stTileCopyMasked #define jlpTilePaste stTilePaste #define jlpTilePasteMono stTilePasteMono + #define jlpTilePasteGlyph stTilePasteGlyph #define jlpTileSnap stTileSnap // Whole-map engine alias (W3 seam): tile.c's jlTileMapPaste // dispatches through this spelling when a port defines it, so the @@ -383,6 +396,7 @@ extern uint16_t iigsGetTickWord(void); #define jlpTileCopyMasked amigaTileCopyMasked #define jlpTilePaste amigaTilePaste #define jlpTilePasteMono amigaTilePasteMono + #define jlpTilePasteGlyph amigaTilePasteGlyph #define jlpTileSnap amigaTileSnap #define jlpTileMapPaste amigaTileMapPaste #elif defined(JOEYLIB_PLATFORM_X68000) @@ -391,6 +405,7 @@ extern uint16_t iigsGetTickWord(void); #define jlpTileCopyMasked x68kTileCopyMasked #define jlpTilePaste x68kTilePaste #define jlpTilePasteMono x68kTilePasteMono + #define jlpTilePasteGlyph x68kTilePasteGlyph #define jlpTileSnap x68kTileSnap #define jlpTileMapPaste x68kTileMapPaste #elif defined(JOEYLIB_PLATFORM_DOS) @@ -532,6 +547,15 @@ void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t * // (tileFill is the opposite: its wrapper does NOT mask, so the jlp layer owns // the & 0x0F there -- see the IIgs macro above and the planar overrides.) void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor); +// Same masking contract as tilePasteMono: fgColor/bgColor arrive already +// masked to 4 bits (the public jlTilePasteGlyph wrapper owns the & 0x0F). +// Declared unconditionally -- the planar overrides fall back to it when a +// surface has no port data. +void jlpGenericTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor); +#if !defined(jlpTilePasteGlyph) + #define jlpTilePasteGlyph(_d, _bx, _by, _b, _fg, _bg) jlpGenericTilePasteGlyph((_d), (_bx), (_by), (_b), (_fg), (_bg)) +#endif + #if !defined(jlpTilePasteMono) #if defined(JL_HAS_TILE_PASTE_MONO) void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor); diff --git a/src/core/sprite.c b/src/core/sprite.c index 9737adf..94b7928 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -400,6 +400,7 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16 uint8_t maxWord = SURFACE_WORD_INDEX((int16_t)((_x) + (int16_t)(_w) - 1)); \ uint8_t *minPtr = &gStageMinWord[(uint16_t)(_y)]; \ uint8_t *maxPtr = &gStageMaxWord[(uint16_t)(_y)]; \ + stageMarkDirtyGroups((uint16_t)(_y), (uint16_t)(_y) + 16u); \ SPRITE_MARK_ROW(0); \ SPRITE_MARK_ROW(1); \ SPRITE_MARK_ROW(2); \ @@ -438,6 +439,7 @@ static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1 maxWord = SURFACE_WORD_INDEX(x + w - 1); minPtr = &gStageMinWord[(uint16_t)y]; maxPtr = &gStageMaxWord[(uint16_t)y]; + stageMarkDirtyGroups((uint16_t)y, (uint16_t)y + (uint16_t)h); if (h == 16) { // The dominant window: every 2-tile-high sprite (all UBER // sprite rows, the gameFrame sprites). diff --git a/src/core/surface.c b/src/core/surface.c index b153a12..552ef3f 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -32,6 +32,11 @@ jlSurfaceT *gStage = NULL; // Harmless on the other ports (plain GNU-C attribute). uint8_t gStageMinWord[SURFACE_HEIGHT] __attribute__((aligned(2))); uint8_t gStageMaxWord[SURFACE_HEIGHT] __attribute__((aligned(2))); +#if defined(JOEYLIB_PLATFORM_IIGS) +// Coarse per-8-row dirty map (surfaceInternal.h): a conservative hint +// that lets the PEI-slam present skip 8 rows at a time. +uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; +#endif // Present-path telemetry; see surfaceInternal.h. Written by the // ST/Amiga/DOS jlpPresent implementations, read by the UBER_PROBE @@ -285,6 +290,15 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) { memset(gStageMinWord, 0, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT); #endif +#if defined(JOEYLIB_PLATFORM_IIGS) + { + uint8_t grp; + + for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) { + gStageDirtyGroup[grp] = 1u; + } + } +#endif } @@ -310,6 +324,7 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma // forms (gcc-mint materializes four live pointers either way), // and the simple loop re-measured exactly at the ST baseline. // DOS was flat either way, so only the Amiga takes the unroll. + stageMarkDirtyGroups(y, yEnd); minPtr = &gStageMinWord[y]; maxPtr = &gStageMaxWord[y]; #if defined(JOEYLIB_PLATFORM_AMIGA) @@ -430,6 +445,19 @@ void stageDirtyClearAll(void) { memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT); #endif +#if defined(JOEYLIB_PLATFORM_IIGS) + // NOT memset: this runs after every present, and llvm-mos lowers a + // memset to a far-called byte loop (~30 cyc/byte) -- the same trap + // the band fill above avoids. Open-coding the 25 stores was worth + // ~800 cycles a present, which was the whole gain of the group map. + { + uint8_t grp; + + for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) { + gStageDirtyGroup[grp] = 0u; + } + } +#endif } diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index 39be831..d954b56 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -119,6 +119,39 @@ extern jlSurfaceT *gStage; extern bool gStageScbDirty; extern bool gStagePaletteDirty; +// Coarse dirty map: one byte per GROUP of 8 stage rows, non-zero when +// any row in the group may be dirty. It is a conservative hint, never +// the authority -- the per-row band test above is -- so an extra set +// byte only costs time. It exists because a present otherwise visits +// all 200 rows to find the handful that changed (measured: 37 dirty +// rows per present in Space Taxi gameplay), and a clean group lets the +// walk step 8 rows at once. Every writer that widens a band must set +// the group byte(s) it covers; stageDirtyClearAll clears them. +// IIgs ONLY. The other ports' presents do not consult it, so they must +// not pay to maintain it: the DOS present already skips four rows at a +// time by testing gStageMinWord a uint32 at a time -- a load-width +// trick that costs the marking side nothing -- and the ST/Amiga walkers +// are asm. The 65816 has no 32-bit load, which is exactly why it needs +// the explicit map instead. Un-gate this if another port's present +// grows a group walk. +#define STAGE_DIRTY_GROUP_SHIFT 3 +#define STAGE_DIRTY_GROUPS (SURFACE_HEIGHT >> STAGE_DIRTY_GROUP_SHIFT) +#if defined(JOEYLIB_PLATFORM_IIGS) +extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; + +// Flag the groups covering rows _y .. _yEnd-1 (yEnd exclusive). Rows +// are already clipped to [0, SURFACE_HEIGHT) by every caller. +#define stageMarkDirtyGroups(_y, _yEnd) do { \ + uint16_t grp_ = (uint16_t)(_y) >> STAGE_DIRTY_GROUP_SHIFT; \ + uint16_t last_ = (uint16_t)((_yEnd) - 1u) >> STAGE_DIRTY_GROUP_SHIFT; \ + for (; grp_ <= last_; grp_++) { \ + gStageDirtyGroup[grp_] = 1u; \ + } \ +} while (0) +#else +#define stageMarkDirtyGroups(_y, _yEnd) ((void)0) +#endif + // Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1 // (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes // straight to the asm marker so a multi-row mark stays a single JSL; @@ -171,6 +204,7 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma int16_t markH_ = (_h); \ uint8_t markMin_ = SURFACE_WORD_INDEX(markX_); \ uint8_t markMax_ = SURFACE_WORD_INDEX(markX_ + (_w) - 1); \ + stageMarkDirtyGroups(markY_, markY_ + markH_); \ if (markH_ == 1) { \ if (markMin_ < gStageMinWord[markY_]) { \ gStageMinWord[markY_] = markMin_; \ diff --git a/src/core/tile.c b/src/core/tile.c index 39e4398..d502207 100644 --- a/src/core/tile.c +++ b/src/core/tile.c @@ -56,10 +56,17 @@ typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1]; // Phase 2 Step D), so their C-side mark compiles out here. // copyMasked/pasteMono still mark separately via tileMarkDirty. #define tileMarkDirtyFused(_pixelY, _bx) ((void)0) +// pasteGlyph's IIgs stage arm fuses the mark the same way (iigsTilePasteGlyphMark). +#if defined(JL_HAS_TILE_PASTE_GLYPH) +#define tileMarkDirtyGlyphFused(_pixelY, _bx) ((void)0) +#else +#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) +#endif #else // On non-IIgs ports nothing fuses the mark, so the "fused" spelling // is the plain helper. #define tileMarkDirtyFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) +#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) // Explicit 8-row unroll: gcc-6.5 -O2 keeps the equivalent loop rolled // (8 iterations of compare + spill-reload, ~670 cycles); the d16(An) // unroll is ~450. Same widen semantics, row by row, as the loop. @@ -80,6 +87,10 @@ static void tileMarkDirty(uint16_t pixelY, uint8_t bx) { minWord = (uint8_t)(bx << 1); maxWord = (uint8_t)(minWord + 1u); + // A whole-tile mark is 8 rows starting at by*8, so it covers + // EXACTLY one dirty group -- one store, no range loop. (This arm is + // non-IIgs, where the map is not maintained; the macro is a no-op.) + stageMarkDirtyGroups(pixelY, pixelY + TILE_PIXELS_PER_SIDE); minPtr = &gStageMinWord[pixelY]; maxPtr = &gStageMaxWord[pixelY]; TILE_MARK_ROW(0); @@ -344,6 +355,22 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) { } +void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + if (dst == NULL || bits == NULL) { + return; + } + if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { + return; + } + fgColor &= 0x0Fu; + bgColor &= 0x0Fu; + jlpTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); + if (dst == gStage) { + tileMarkDirtyGlyphFused(TILE_BLOCK_TO_PIXEL(by), bx); + } +} + + void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) { if (dst == NULL || in == NULL) { return; diff --git a/src/generic/genericTile.c b/src/generic/genericTile.c index d1127ed..a6abf85 100644 --- a/src/generic/genericTile.c +++ b/src/generic/genericTile.c @@ -108,6 +108,35 @@ void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint } +// Paste an 8x8 1bpp glyph into surface block (bx,by): bits[row] is one +// glyph row, msb leftmost. Two bits make one chunky byte, so a 4-entry +// table of the fg/bg combinations keeps the inner loop branch-free and +// the expansion happens straight into the surface -- no intermediate +// mono tile and no second paste call. +void jlpGenericTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + uint8_t combo[4]; + uint8_t *d; + uint8_t row; + + if (dst->pixels == NULL) { + return; + } + combo[0] = (uint8_t)((bgColor << 4) | bgColor); + combo[1] = (uint8_t)((bgColor << 4) | fgColor); + combo[2] = (uint8_t)((fgColor << 4) | bgColor); + combo[3] = (uint8_t)((fgColor << 4) | fgColor); + d = GENERIC_TILE_ROW0(dst->pixels, bx, by); + for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) { + uint8_t b = bits[row]; + d[0] = combo[b >> 6]; + d[1] = combo[(b >> 4) & 3u]; + d[2] = combo[(b >> 2) & 3u]; + d[3] = combo[b & 3u]; + d += SURFACE_BYTES_PER_ROW; + } +} + + // Paste a chunky 8x8 tile (32 bytes, 4-bytes/row) into surface block (bx,by). void jlpGenericTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) { uint8_t *d; diff --git a/src/iigs/hal.c b/src/iigs/hal.c index 86df021..3a155ab 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -194,6 +194,20 @@ volatile uint16_t gPeiRunLen; // slam run: row count (1..40) volatile uint16_t gPeiRunMax; // slam run: rightmost dirty word (low byte valid) volatile uint16_t gPeiRunBytes; // slam run: bytes pushed per row volatile uint16_t gPeiWidthTmp; // scan: band width scratch (low byte valid) +// Narrow-row MVN RUN (rows sharing one identical band are set up once). +volatile uint16_t gPeiBandMin; // the run's band, low byte valid +volatile uint16_t gPeiBandMax; +volatile uint16_t gPeiResumeRow; // first row AFTER the run +volatile uint16_t gPeiResumeChunk; +volatile uint16_t gPeiRunOff; // stage byte offset of the run's first row +volatile uint16_t gPeiRunEnd; // offset one row past the run's last +volatile uint16_t gPeiRunStride; // 160 - bytes: advance after an MVN +volatile uint16_t gPeiRunRows; // rows in the run +volatile uint16_t gPeiMulA; // run telemetry: bytes * rows +volatile uint16_t gPeiMulB; +volatile uint16_t gPeiMulOut; +volatile uint16_t gStageGrpTmp; // asm markers: first coarse group index +volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group // Upload SCB / palette into bank-$E1 SHR memory only when the diff --git a/src/iigs/joeyDraw.s b/src/iigs/joeyDraw.s index d8861fc..ae2349f 100644 --- a/src/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -1234,6 +1234,362 @@ tpmCombo = 8 rtl +; ==================================================================== +; iigsTilePasteGlyphMark(const uint8_t *bits, uint16_t bx, uint16_t by, +; uint16_t fg, uint16_t bg) +; +; Paste an 8x8 ONE-BIT-PER-PIXEL glyph into the stage and fuse the +; dirty mark, in one JSL. This is the shape a character generator +; already has, so unlike iigsTilePasteMonoInner nothing has to expand +; the glyph into a 32-byte chunky silhouette first -- the caller hands +; over the eight glyph rows as they stand. +; +; Design: a glyph NIBBLE is four pixels = two output bytes = one word, +; so a per-call 16-entry word table turns each row into two indexed +; loads and two word stores. That halves the stores against the mono +; path's byte-at-a-time expansion AND -- because the glyph bytes are +; spread into words up front -- the whole body stays in M=16, with no +; sep/rep churn per pixel pair (the mode flips alone cost ~96 cyc a +; tile). Measured whole-call cost against the C generic it replaces: +; roughly 930 cycles versus ~2300. +; +; The table: pb[m] is the output byte for a 2-bit pixel pair (high bit +; = left pixel), so tpgWord[n] = pb[n & 3] << 8 | pb[n >> 2] -- the low +; byte of the word lands at dst+0 (pixels 0,1) and the high byte at +; dst+1 (pixels 2,3), which is what little-endian word stores give. +; +; fg/bg arrive PRE-MASKED to 4 bits (the public jlTilePasteGlyph +; wrapper owns the & 0x0F), same contract as tilePasteMono. +; +; ABI: arg0 bits in A:X; bx @4,s by @6,s fg @8,s bg @10,s on JSL entry +; -> @8/@10/@12/@14 after php+phb+phd -> @12/@14/@16/@18 after the +; 4-byte source D-frame. Dst is $012000+off,x like +; iigsTilePasteMark. DBR is the caller's data bank throughout (no +; plb), so the absolute scratch below and gRowOffsetLut both read +; with plain abs addressing. +; ==================================================================== +; Emitted into .text.iigsFusedTile, NOT a section of its own: the fused +; mark below is a 16-bit `jsr stageTileMark`, and a jsr between two +; .section blocks silently jumps into another bank. +.section .text.iigsFusedTile,"ax" +.globl iigsTilePasteGlyphMark +iigsTilePasteGlyphMark: + php + phb + phd + rep #0x30 + .a16 + .i16 + phx ; bits high16 -> 3,s + pha ; bits low16 -> 1,s + tsc + inc a + tcd ; [0] = bits + + ; Spread the eight glyph bytes into words so the body + ; can index the table without ever leaving M=16. + ldy #0 + lda [0], y + and #0x00FF + sta tpgBitsW+0 + lda [0], y + xba + and #0x00FF + sta tpgBitsW+2 + ldy #2 + lda [0], y + and #0x00FF + sta tpgBitsW+4 + lda [0], y + xba + and #0x00FF + sta tpgBitsW+6 + ldy #4 + lda [0], y + and #0x00FF + sta tpgBitsW+8 + lda [0], y + xba + and #0x00FF + sta tpgBitsW+10 + ldy #6 + lda [0], y + and #0x00FF + sta tpgBitsW+12 + lda [0], y + xba + and #0x00FF + sta tpgBitsW+14 + + ; pb[0..3]: the output byte for each 2-bit pixel pair, + ; and the same four values pre-shifted into a high byte. + lda 16,s ; fg + asl a + asl a + asl a + asl a + sta tpgFgHi ; fg << 4 + lda 18,s ; bg + asl a + asl a + asl a + asl a + sta tpgBgHi ; bg << 4 + lda tpgBgHi + ora 18,s + sta tpgPb+0 ; bg:bg + xba + sta tpgPbHi+0 + lda tpgBgHi + ora 16,s + sta tpgPb+2 ; bg:fg + xba + sta tpgPbHi+2 + lda tpgFgHi + ora 18,s + sta tpgPb+4 ; fg:bg + xba + sta tpgPbHi+4 + lda tpgFgHi + ora 16,s + sta tpgPb+6 ; fg:fg + xba + sta tpgPbHi+6 + + ; tpgWord[hi*4 + lo] = pb[lo] << 8 | pb[hi] + lda tpgPbHi+0 + ora tpgPb+0 + sta tpgWord+0 + lda tpgPbHi+2 + ora tpgPb+0 + sta tpgWord+2 + lda tpgPbHi+4 + ora tpgPb+0 + sta tpgWord+4 + lda tpgPbHi+6 + ora tpgPb+0 + sta tpgWord+6 + lda tpgPbHi+0 + ora tpgPb+2 + sta tpgWord+8 + lda tpgPbHi+2 + ora tpgPb+2 + sta tpgWord+10 + lda tpgPbHi+4 + ora tpgPb+2 + sta tpgWord+12 + lda tpgPbHi+6 + ora tpgPb+2 + sta tpgWord+14 + lda tpgPbHi+0 + ora tpgPb+4 + sta tpgWord+16 + lda tpgPbHi+2 + ora tpgPb+4 + sta tpgWord+18 + lda tpgPbHi+4 + ora tpgPb+4 + sta tpgWord+20 + lda tpgPbHi+6 + ora tpgPb+4 + sta tpgWord+22 + lda tpgPbHi+0 + ora tpgPb+6 + sta tpgWord+24 + lda tpgPbHi+2 + ora tpgPb+6 + sta tpgWord+26 + lda tpgPbHi+4 + ora tpgPb+6 + sta tpgWord+28 + lda tpgPbHi+6 + ora tpgPb+6 + sta tpgWord+30 + + ; X = stage byte offset of the tile's row 0 + lda 14,s ; by + asl a + asl a + asl a + asl a + tax + lda 12,s ; bx + asl a + asl a + clc + adc gRowOffsetLut,x + tax + + ; row 0 + lda tpgBitsW+0 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+0,x + lda tpgBitsW+0 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+2,x + ; row 1 + lda tpgBitsW+2 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+160,x + lda tpgBitsW+2 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+162,x + ; row 2 + lda tpgBitsW+4 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+320,x + lda tpgBitsW+4 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+322,x + ; row 3 + lda tpgBitsW+6 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+480,x + lda tpgBitsW+6 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+482,x + ; row 4 + lda tpgBitsW+8 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+640,x + lda tpgBitsW+8 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+642,x + ; row 5 + lda tpgBitsW+10 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+800,x + lda tpgBitsW+10 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+802,x + ; row 6 + lda tpgBitsW+12 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+960,x + lda tpgBitsW+12 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+962,x + ; row 7 + lda tpgBitsW+14 + lsr a + lsr a + lsr a + and #0x001E ; (b >> 4) * 2 + tay + lda tpgWord, y + sta 0x012000+1120,x + lda tpgBitsW+14 + and #0x000F + asl a ; (b & 15) * 2 + tay + lda tpgWord, y + sta 0x012000+1122,x + + ; fused dirty mark + lda 14,s + tay ; Y = by + lda 12,s ; A = bx + jsr stageTileMark + + ; drop the source D-frame and restore + tsc + clc + adc #4 + tcs + pld + plb + plp + rtl + + +; iigsTilePasteGlyphMark per-call scratch. +.section .bss.tpgBitsW,"aw" +.globl tpgBitsW +tpgBitsW: +.zero 16 + +.section .bss.tpgWord,"aw" +.globl tpgWord +tpgWord: +.zero 32 + +.section .bss.tpgPb,"aw" +.globl tpgPb +tpgPb: +.zero 8 + +.section .bss.tpgPbHi,"aw" +.globl tpgPbHi +tpgPbHi: +.zero 8 + +.section .bss.tpgFgHi,"aw" +.globl tpgFgHi +tpgFgHi: +.zero 2 + +.section .bss.tpgBgHi,"aw" +.globl tpgBgHi +tpgBgHi: +.zero 2 + + ; iigsTilePasteMonoInner per-call scratch (fg<<4 / bg<<4 staging). .section .bss.tpmFgHi,"aw" .globl tpmFgHi @@ -1443,6 +1799,12 @@ stageTileMark: pla ; A = minWord (high byte 0) sep #0x20 .a8 + ; Coarse group flag: 8 rows starting at by*8 are exactly + ; group `by`, and Y still holds by. One store, no shift. + pha ; park minWord + lda #1 + sta gStageDirtyGroup,y + pla ldy #8 ; row counter (X stays 16-bit) stmLoop: cmp gStageMinWord,x @@ -2041,6 +2403,41 @@ tmaskOutLo: .section .text.iigsFusedRect,"ax" .globl stageRectMark stageRectMark: + ; Coarse group flags for rows X .. X+Y-1, before the band + ; walk consumes the registers. A holds the band, X yStart, + ; Y rowCount -- all three are restored below. + pha ; band -> 5,s + phx ; yStart -> 3,s + phy ; rowCount -> 1,s + txa + lsr a + lsr a + lsr a + sta gStageGrpTmp ; first group + txa + clc + adc 1,s ; + rowCount + dec a ; last row + lsr a + lsr a + lsr a ; last group + sec + sbc gStageGrpTmp + tay ; Y = group count - 1 + ldx gStageGrpTmp + sep #0x20 + .a8 + lda #1 +strmGrpLoop: + sta gStageDirtyGroup,x + inx + dey + bpl strmGrpLoop + rep #0x20 + .a16 + ply ; rowCount + plx ; yStart + pla ; band sep #0x20 .a8 strmLoop: @@ -4331,8 +4728,50 @@ peiRowLoop: peiMvnRowBr: brl peiMvnRow peiRowClean: +; The row is clean. Before stepping one row, consult the coarse group +; flag (surfaceInternal.h gStageDirtyGroup): a clean 8-row group lets +; the walk jump straight to the next group boundary instead of testing +; seven more bands one at a time. Measured ~37 dirty rows per present, +; so most of the 200 row visits were pure scan. The flag is only ever a +; HINT -- a set byte means "may be dirty" and the band test above stays +; the authority -- so a stale-set flag costs time, never correctness. +; The index maths runs in M=16 throughout: TAX/TAY there move a clean +; 16-bit value, whereas in M=8 they would carry whatever the hidden +; high byte B happens to hold. + phx ; park the row + rep #0x20 + .a16 + txa + lsr a + lsr a + lsr a ; A = group index (row < 200) + tax + sep #0x20 + .a8 + lda gStageDirtyGroup,x + plx ; row back in X (PLX sets flags on X) + cmp #0 + beq peiRowSkipGroup inx iny + bra peiRowBounds +peiRowSkipGroup: + rep #0x20 + .a16 + txa + sta gPeiSkipTmp ; old row + ora #7 + inc a ; A = next group boundary + tax + sec + sbc gPeiSkipTmp ; A = rows skipped + sta gPeiSkipTmp + tya + clc + adc gPeiSkipTmp + tay ; the chunk budget counts them too + sep #0x20 + .a8 peiRowBounds: cpx #200 bcs peiChunkDone @@ -4610,29 +5049,84 @@ peiRunCntNext: ; read. GAS spells MVN dst,src so `mvn 0xE1, 0x01`. ; --------------------------------------------------------------- peiMvnRow: ; a8; X = row, Y = chunk count +; --------------------------------------------------------------- +; Narrow-row MVN RUN. A row costs ~229 cyc here of which the MVN +; itself is only ~7/byte, and the measured band is ~12 bytes wide -- +; so five sixths of a narrow row was setup: spilling X/Y around the +; MVN (which clobbers both), recomputing the count and the row +; offset, restoring DBR from the phb copy, and a 32-bit telemetry add. +; But consecutive rows almost always carry the IDENTICAL band -- one +; painted cell marks 8 rows with the same min/max, a sprite 24 -- +; measured at 37 dirty rows in under 4 distinct bands per present, +; runs of ~10. So gather the run first and pay that setup once: the +; inner loop is then one MVN plus a stride add, and it never touches +; a DBR-relative global (MVN leaves DBR = $E1, so the loop's three +; live values are read STACK-relative, which ignores DBR). + lda gStageMinWord,x + sta gPeiBandMin + lda gStageMaxWord,x + sta gPeiBandMax rep #0x20 .a16 txa - sta gPeiCurRow + sta gPeiCurRow ; first row of the run tya sta gPeiChunkRow - txa - asl a - tay ; Y = LUT byte index (X = row kept) sep #0x20 .a8 +peiMvnGather: +; Extend while the next row is dirty with the SAME band and the chunk +; budget allows. Identical band implies identical width, so every row +; gathered is narrow like the first -- a wide row can never join. + inx + iny + cpx #200 + bcs peiMvnRunGo + cpy #40 + bcs peiMvnRunGo + lda gStageMinWord,x + cmp gPeiBandMin + bne peiMvnRunGo lda gStageMaxWord,x + cmp gPeiBandMax + bne peiMvnRunGo + bra peiMvnGather + +peiMvnRunGo: +; X / Y already point one past the run: that is the scan's resume point. + rep #0x20 + .a16 + txa + sta gPeiResumeRow + tya + sta gPeiResumeChunk sec - sbc gStageMinWord,x + sbc gPeiChunkRow + sta gPeiRunRows ; rows = resumeChunk - chunkRow + ; count = (max - min) * 2 + 1 (the MVN arg is bytes - 1) + sep #0x20 + .a8 + lda gPeiBandMax + sec + sbc gPeiBandMin rep #0x20 .a16 and #0x00FF asl a inc a - sta gPeiMvnCount ; MVN count arg = bytes - 1 + sta gPeiMvnCount + ; stride to the next row = 160 - bytes = 159 - count + lda #159 + sec + sbc gPeiMvnCount + sta gPeiRunStride + ; first offset = min*2 + gRowOffsetLut[firstRow] + $2000 + lda gPeiCurRow + asl a + tay sep #0x20 .a8 - lda gStageMinWord,x + lda gPeiBandMin rep #0x20 .a16 and #0x00FF @@ -4640,10 +5134,47 @@ peiMvnRow: ; a8; X = row, Y = chunk count clc adc gRowOffsetLut,y adc #0x2000 ; no carry possible: sum < $9D00 - tax - txy ; src offset == dst offset + sta gPeiRunOff + ; end = first + rows*160 (160 == 128 + 32) + lda gPeiRunRows + asl a + asl a + asl a + asl a + asl a + sta gPeiMulA ; rows * 32 + asl a + asl a ; rows * 128 + clc + adc gPeiMulA + clc + adc gPeiRunOff + sta gPeiRunEnd +; Park count / stride / end on the STACK: MVN leaves DBR = $E1, so an +; absolute read inside the loop would land in the wrong bank. Stack- +; relative addressing ignores DBR. NOTE the phb copy moves to 9,s while +; these three are pushed -- the DBR restore below happens after the pulls. + ldx gPeiRunOff + txy + lda gPeiRunEnd + pha ; -> 5,s + lda gPeiRunStride + pha ; -> 3,s lda gPeiMvnCount - mvn 0xE1, 0x01 + pha ; -> 1,s +peiMvnRunLoop: + lda 1,s ; count + mvn 0xE1, 0x01 ; X and Y advance by count+1 + txa + clc + adc 3,s ; + (160 - bytes) = next row + tax + tay + cmp 5,s ; reached one row past the last? + bne peiMvnRunLoop + pla + pla + pla ; MVN left DBR=$E1; restore the caller's (= globals) bank ; from the phb copy. FRAME-LAYOUT DEPENDENCY: 3,s is the ; phb byte ONLY while nothing extra is pushed around the @@ -4656,24 +5187,38 @@ peiMvnRow: ; a8; X = row, Y = chunk count plb rep #0x20 .a16 - ; telemetry (W2 Stage 0): the MVN moved gPeiMvnCount + 1 - ; bytes (the MVN count argument is bytes - 1); sec + adc - ; adds the + 1. sta does not touch carry, so the bcc - ; still tests the add. +; telemetry (W2 Stage 0): the run moved bytes * rows. Shift-add, once +; per run rather than a 32-bit add per row. + lda gPeiMvnCount + inc a + sta gPeiMulA ; multiplicand = bytes + lda #0 + sta gPeiMulOut + lda gPeiRunRows + sta gPeiMulB +peiMulLoop: + lsr gPeiMulB + bcc peiMulSkip + lda gPeiMulOut + clc + adc gPeiMulA + sta gPeiMulOut +peiMulSkip: + asl gPeiMulA + lda gPeiMulB + bne peiMulLoop lda gPresentCopiedBytes - sec - adc gPeiMvnCount + clc + adc gPeiMulOut sta gPresentCopiedBytes bcc peiMvnCntDone inc gPresentCopiedBytes+2 peiMvnCntDone: -; Restore row counters and advance. - lda gPeiCurRow +; Resume the scan one row past the run (X/Y were advanced by the gather). + lda gPeiResumeRow tax - inx - lda gPeiChunkRow + lda gPeiResumeChunk tay - iny sep #0x20 .a8 brl peiRowBounds @@ -4743,6 +5288,34 @@ mdrMaxWord = 6 cpx mdrYEnd bcs mdrExit ; empty range -> nothing to do + ; Coarse group flags for rows X .. yEnd-1 (X is preserved). + phx + txa + lsr a + lsr a + lsr a + sta gStageGrpTmp ; first group + lda mdrYEnd + dec a + lsr a + lsr a + lsr a ; last group + sec + sbc gStageGrpTmp + tay ; Y = group count - 1 + ldx gStageGrpTmp + sep #0x20 + .a8 + lda #1 +mdrGrpLoop: + sta gStageDirtyGroup,x + inx + dey + bpl mdrGrpLoop + rep #0x20 + .a16 + plx + sep #0x20 .a8 diff --git a/src/x68000/x68kTile.h b/src/x68000/x68kTile.h index 5feff35..2971b58 100644 --- a/src/x68000/x68kTile.h +++ b/src/x68000/x68kTile.h @@ -309,13 +309,32 @@ static inline __attribute__((always_inline)) void x68kTileMapPaste(jlSurfaceT *d // writes // outPlaneK = (shape & maskFgK) | (~shape & maskBgK) // where maskXK = $FF if (X & (1 << k)) else $00. +// Eight 1bpp shape rows into the bitplanes: set bits take the +// foreground's plane bit, clear bits the background's. Shared by the +// mono paste (which folds a chunky silhouette into these rows first) +// and the glyph paste (whose input already IS these rows). +static inline __attribute__((always_inline)) void x68kTilePasteShape(X68kPlanarT *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { + uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; + uint8_t plane; + uint8_t row; + + for (plane = 0; plane < X68K_BITPLANES; plane++) { + uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t *p = pd->planes[plane] + rowBase; + for (row = 0; row < 8u; row++) { + uint8_t s = shape[row]; + p[row * X68K_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); + } + } +} + + static inline __attribute__((always_inline)) void x68kTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { X68kPlanarT *pd; uint8_t shape[TILE_PIXELS_PER_SIDE]; - uint8_t plane; uint8_t row; uint8_t col; - uint16_t rowBase; pd = (X68kPlanarT *)dst->portData; if (pd == NULL) { @@ -337,16 +356,21 @@ static inline __attribute__((always_inline)) void x68kTilePasteMono(jlSurfaceT * } shape[row] = bits; } - rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; - for (plane = 0; plane < X68K_BITPLANES; plane++) { - uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t *p = pd->planes[plane] + rowBase; - for (row = 0; row < 8u; row++) { - uint8_t s = shape[row]; - p[row * X68K_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); - } + x68kTilePasteShape(pd, bx, by, shape, fgColor, bgColor); +} + + +// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape +// the mono paste has to fold out of a chunky silhouette, so this skips +// the fold entirely (and cannot be handed the wrong tile format). +static inline __attribute__((always_inline)) void x68kTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + X68kPlanarT *pd = (X68kPlanarT *)dst->portData; + + if (pd == NULL) { + jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); + return; } + x68kTilePasteShape(pd, bx, by, bits, fgColor, bgColor); } #endif diff --git a/tools/scanPtrBug.py b/tools/scanPtrBug.py new file mode 100755 index 0000000..290c0fa --- /dev/null +++ b/tools/scanPtrBug.py @@ -0,0 +1,48 @@ +# Detect the w65816 codegen bug where a far pointer's 16-bit OFFSET half is +# built from the pointer's BANK register. +# +# The bank register is the DP slot the prologue fills with `stx ` (the +# incoming pointer arrives as offset-in-A, bank-in-X). Flagging a +# `lda ... sta 0xe0` means the pointer used for the following +# [dp],y access has the bank value in its offset half -- it reads a stray +# address. Correct code reloads A with the offset (pla / txa / lda n,s) +# before storing to 0xe0. +import re, sys + +BOILER = re.compile(r'(sty\s+0xfa|ldy\s+0xfa|ldy\s+#|sta\s+\[0x[0-9a-f]+\], y|lda\s+\[0x[0-9a-f]+\], y|nop)') + +def scan(path): + lines = [l.strip() for l in open(path).read().split("\n")] + # function start lines + starts = [i for i, l in enumerate(lines) if re.match(r'^[A-Za-z_][A-Za-z0-9_]*:', l)] + bounds = list(zip(starts, starts[1:] + [len(lines)])) + hits = [] + for s, e in bounds: + name = lines[s].split(":")[0] + bank = None + for i in range(s, min(s + 40, e)): + m = re.match(r'stx\s+(0x[0-9a-f]+)$', lines[i]) + if m: + bank = m.group(1) + break + if bank is None: + continue + for i in range(s, e): + if not re.match(r'sta\s+0x(e0|e4|e8|ec)$', lines[i]): + continue + j = i - 1 + while j > s and BOILER.match(lines[j]): + j -= 1 + if lines[j] == "lda\t%s" % bank or lines[j] == "lda %s" % bank: + hits.append((name, i + 1, bank, lines[i])) + return hits + +bad = 0 +for p in sys.argv[1:]: + h = scan(p) + if h: + bad += len(h) + print("%s: %d" % (p, len(h))) + for fn, ln, bk, u in h: + print(" %-28s line %-6d bank=%s -> %s" % (fn, ln, bk, u)) +print("total suspect pointer builds: %d" % bad) diff --git a/tools/xdftool.py b/tools/xdftool.py index 44e7aee..44cf966 100755 --- a/tools/xdftool.py +++ b/tools/xdftool.py @@ -20,6 +20,8 @@ Usage: xdftool.py list xdftool.py add [name] xdftool.py delete + xdftool.py deltree + xdftool.py empty xdftool.py extract xdftool.py free """ @@ -185,8 +187,26 @@ class Xdf: return None return None + def find_at_path(self, path): + # Resolve a file at any depth ("DATA/LEVELS/X.DAT") by walking the directory chain. + head, _, leaf = path.rpartition("/") + cluster = self.dir_cluster(head) if head else 0 + if cluster is None: + return None + main11, ext10 = self.encode_name_ext(leaf) + for _, _, e in self._entries_in_dir(cluster): + if e[0] == FREE_MARKER: + return None + if e[0] == DELETED_MARKER or (e[11] & (ATTR_VOLUME | ATTR_DIR)): + continue + if self.entry_matches(e, main11, ext10): + return e + return None + def read_file(self, name): - if "/" in name: # SAVES/RN.CFG: resolve through the subdirectory + if name.count("/") > 1: # DATA/LEVELS/X.DAT: walk the whole chain + e = self.find_at_path(name) + elif "/" in name: # SAVES/RN.CFG: resolve through the subdirectory sub, _, leaf = name.partition("/") e = self.find_in_subdir(sub, leaf) else: @@ -218,6 +238,78 @@ class Xdf: self.data[off] = DELETED_MARKER return True + + def free_chain(self, cluster): + # Release a FAT chain. Guarded against a self-referential chain in a + # damaged image, which would otherwise spin forever. + guard = 0 + while 2 <= cluster < EOC_MIN: + nxt = self.fat_get(cluster) + self.fat_set(cluster, 0) + cluster = nxt + guard += 1 + if guard > self.max_cluster: + break + + def delete_tree(self, name): + # Delete a ROOT entry and everything under it. A stock Human68k system + # floppy keeps ~900 KB of utilities in subdirectories (SYS, BIN, ...), + # and `list`/`delete` only ever see files, so emptying one by deleting + # the root FILES leaves all of that still allocated -- the reason a + # 1232 KB disk looked like it had 330 KB to give. + for i, off, e in self._entries(): + if e[0] == FREE_MARKER: + break + if e[0] == DELETED_MARKER or (e[11] & ATTR_VOLUME): + continue + main11, ext10 = self.encode_name_ext(name) + if not self.entry_matches(e, main11, ext10): + continue + cluster = struct.unpack(" self.max_cluster: + return + + def _put_entry_any(self, cluster, entry): + # Root-aware wrapper around _put_entry_in_dir, which can only extend a cluster chain. + if cluster == 0: + for _, off, e in self._entries(): + if e[0] in (FREE_MARKER, DELETED_MARKER): + self.data[off:off + DIR_ENTRY_SIZE] = entry + return + raise OSError(f"{self.path}: root directory full") + self._put_entry_in_dir(cluster, entry) + def _put_entry_in_dir(self, first_cluster, entry): # Write a 32-byte directory `entry` into the first FREE/DELETED slot of the directory whose data # begins at first_cluster, extending the cluster chain by one when every slot is already used. @@ -299,6 +422,53 @@ class Xdf: struct.pack_into(" 8: + raise ValueError(f"subdir '{part}' does not fit 8.3") + stem = part.ljust(8).encode("ascii") + b" " + found = None + for _, _, e in self._entries_in_dir(cluster): + if e[0] == FREE_MARKER: + break + if e[0] == DELETED_MARKER or not (e[11] & ATTR_DIR): + continue + if bytes(e[0:11]) == stem: + found = struct.unpack(" create the chain, then write the leaf into the last dir. + head_dir, _, leaf = path.rpartition("/") + cluster = self.dir_cluster(head_dir, create=True) if head_dir else 0 + main11, ext10 = self.encode_name_ext(leaf) + first = self._alloc_write_payload(payload) + self._put_entry_any(cluster, self._dir_entry(main11, ATTR_ARCHIVE, first, len(payload), ext10)) + def add_file_in_subdir(self, dirname, name, payload): # Write `name` into subdirectory `dirname` (creating SUB/. and SUB/.. when the subdir is new), so a # guest reading e.g. SAVES/RN.CFG or SAVES/ finds it. If the subdir ALREADY exists (a second @@ -374,7 +544,9 @@ def main(argv): img = Xdf(image, writable=True) with open(host, "rb") as fp: payload = fp.read() - if "/" in name: # SAVES/RN.CFG -> a one-cluster subdirectory + the file + if name.count("/") > 1: # DATA/LEVELS/X.DAT -> create the whole chain + img.add_file_at_path(name, payload) + elif "/" in name: # SAVES/RN.CFG -> a one-cluster subdirectory + the file sub, _, leaf = name.partition("/") img.add_file_in_subdir(sub, leaf, payload) else: @@ -395,6 +567,28 @@ def main(argv): print(f"deleted {argv[3]} from {image}") return 0 + if cmd == "deltree": + if len(argv) < 4: + print("usage: xdftool.py deltree ", file=sys.stderr) + return 2 + img = Xdf(image, writable=True) + if not img.delete_tree(argv[3]): + print(f"{argv[3]} not in {image}", file=sys.stderr) + return 1 + img.flush() + print(f"deleted tree {argv[3]} from {image}") + return 0 + + if cmd == "empty": + if len(argv) < 3: + print("usage: xdftool.py empty ", file=sys.stderr) + return 2 + img = Xdf(image, writable=True) + n = img.empty_root() + img.flush() + print(f"emptied {image} ({n} root entries removed)") + return 0 + if cmd == "extract": if len(argv) < 5: print("usage: xdftool.py extract ", file=sys.stderr)