Space Taxi work.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 129 B |
|
|
@ -22,11 +22,14 @@
|
|||
#include "spacetaxi.h"
|
||||
#include "stDemoStreams.h"
|
||||
|
||||
// Diagnostics. On the IIgs the logger drags in vsnprintf and an 8 KB
|
||||
// ring buffer that the bank-0 BSS budget cannot spare, and the game has
|
||||
// no need of them there, so logging compiles to nothing on the 65816
|
||||
// and stays on the other ports.
|
||||
#if defined(__W65816__)
|
||||
// Diagnostics. The logger drags in vsnprintf (with newlib that pulls the
|
||||
// whole formatted-output and float-conversion family, ~45 KB) plus a
|
||||
// multi-KB ring buffer, and the game has no need of either. That is
|
||||
// unaffordable on the IIgs, whose bank-0 BSS budget cannot spare the
|
||||
// ring, and on the X68000, which ships on a Human68k floppy with about
|
||||
// 330 KB of usable space for the binary AND its levels. Both compile it
|
||||
// out; the ports with room keep it.
|
||||
#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000)
|
||||
#define ST_LOG_RESET() ((void)0)
|
||||
#define ST_LOG(...) ((void)0)
|
||||
#else
|
||||
|
|
@ -165,6 +168,7 @@ static void enterTitle(void) {
|
|||
}
|
||||
stTitleEnter(&gSim, &gLevel);
|
||||
stRenderSceneChanged(&gSim);
|
||||
stRenderTitleLogo(&gSim);
|
||||
stAudioNoise(false);
|
||||
stAudioSilence();
|
||||
gGame.state = ST_STATE_TITLE;
|
||||
|
|
@ -617,6 +621,7 @@ int main(void) {
|
|||
memset(&gGame, 0, sizeof(gGame));
|
||||
memset(&gSim, 0, sizeof(gSim));
|
||||
stRenderInit(stage);
|
||||
stRenderLoading(stage);
|
||||
stAudioInit();
|
||||
// Boot: the playback buffer as the C64 leaves it, and the state a
|
||||
// fresh machine has when the first title intro starts ($4092 + the
|
||||
|
|
|
|||
|
|
@ -21,10 +21,20 @@ bool stLevelLoad(StLevelT *out, const char *path);
|
|||
// Renderer: paints dirty screen cells with the live charset and draws
|
||||
// the eight sprites of the last marshaled frame with save-under.
|
||||
void stRenderInit(jlSurfaceT *stage);
|
||||
// A word on the stage before the slow part of startup: loading the cel
|
||||
// bank and the first level takes long enough that the host's desktop
|
||||
// would otherwise sit there with nothing to look at.
|
||||
void stRenderLoading(jlSurfaceT *stage);
|
||||
void stRenderShutdown(void);
|
||||
void stRenderFrame(jlSurfaceT *stage, StSimT *sim);
|
||||
// Drop the sprite cache and force a full repaint (new scene).
|
||||
// Drop the sprite cache and force a full repaint (new scene). Also
|
||||
// leaves the title's logo mode (below).
|
||||
void stRenderSceneChanged(StSimT *sim);
|
||||
// Title screen only: draw the logo's four flip-book frames from one
|
||||
// tile held in reserved palette slots, so the flip and the colour
|
||||
// cycle are palette writes instead of repainting 103 (and 407) cells.
|
||||
// Call after stRenderSceneChanged, which turns it back off.
|
||||
void stRenderTitleLogo(StSimT *sim);
|
||||
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
|
||||
|
||||
// Audio sink setup and the per-tick release timers ($4320).
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ static void hookHSegment(StSimT *sim, uint8_t id) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// Level T "FAST BREAK" $7D9D -- the barrier on row 3: `ch` into the
|
||||
// centre opening (cols 18..21), its complement into the side gaps.
|
||||
static void hookTGate(StSimT *sim, uint8_t ch) {
|
||||
|
|
@ -306,8 +307,8 @@ static void hookWLaserUpdate(StSimT *sim) {
|
|||
for (k = 0u; k < 16u; k++) {
|
||||
sim->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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <string.h>
|
||||
|
||||
|
|
@ -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;
|
||||
if (sim->charDirtyAll) {
|
||||
sim->charDirtyAll = false;
|
||||
sim->charDirtyCount = 0u;
|
||||
return;
|
||||
}
|
||||
sim->charDirty[ch] = 0u;
|
||||
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];
|
||||
// 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);
|
||||
}
|
||||
tileFromChunky(tile, chunky);
|
||||
memcpy(gRender.glyphBits[slot], bits, 8u);
|
||||
gRender.glyphUsed[slot] = true;
|
||||
return tile;
|
||||
}
|
||||
if (mask != 0u && slotOfMask[mask] == 0u) {
|
||||
if (gRender.logoSlotCount == ST_LOGO_PAL_SLOTS) {
|
||||
return false;
|
||||
}
|
||||
slotOfMask[mask] = kLogoPalSlot[gRender.logoSlotCount];
|
||||
gRender.logoSlotMask[gRender.logoSlotCount] = mask;
|
||||
gRender.logoSlotCount++;
|
||||
}
|
||||
slot = slotOfMask[mask];
|
||||
if ((col & 1u) == 0u) {
|
||||
out[col >> 1] = (uint8_t)(slot << 4);
|
||||
} else {
|
||||
out[col >> 1] |= slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
tileFromChunky(&gRender.logoTile, chunky);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Which flip-book frame the live logo glyph holds, or ST_LOGO_FRAMES if
|
||||
// it matches none of them (then the logo tile does not stand for what
|
||||
// the simulation is showing and the cells must be repainted normally).
|
||||
static uint8_t logoFrameIndex(const StSimT *sim) {
|
||||
uint8_t f;
|
||||
|
||||
for (f = 0u; f < ST_LOGO_FRAMES; f++) {
|
||||
if (memcmp(sim->charset[ST_LOGO_CHAR], sim->charset[ST_LOGO_FRAME_FIRST + f], 8u) == 0) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return ST_LOGO_FRAMES;
|
||||
}
|
||||
|
||||
|
||||
// Show flip-book frame `frame` in the logo's current colour: every
|
||||
// reserved slot lights up in the colour if that frame is in its set and
|
||||
// goes to the background colour if it is not. The decoration sprites'
|
||||
// slot follows their own colour, which the cycle sets one step behind
|
||||
// the logo's (the title's sprite table overwrites it on entry).
|
||||
static void logoPaletteApply(const StSimT *sim, uint8_t frame) {
|
||||
uint16_t pal[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t on = kC64Palette[sim->logoColor & 15u];
|
||||
uint16_t off = kC64Palette[sim->bgColor & 15u];
|
||||
uint8_t k;
|
||||
|
||||
jlPaletteGet(gRender.stage, 0u, pal);
|
||||
for (k = 0u; k < gRender.logoSlotCount; k++) {
|
||||
pal[kLogoPalSlot[k]] = (((gRender.logoSlotMask[k] >> frame) & 1u) != 0u) ? on : off;
|
||||
}
|
||||
pal[ST_PAL_LOGO_SPR] = kC64Palette[sim->frame.color[ST_LOGO_SPR_FIRST] & 15u];
|
||||
jlPaletteSet(gRender.stage, 0u, pal);
|
||||
gRender.logoFrame = frame;
|
||||
gRender.logoColor = sim->logoColor;
|
||||
gRender.logoSprColor = sim->frame.color[ST_LOGO_SPR_FIRST];
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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,13 +609,11 @@ 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) {
|
||||
if (same && want && dirtyUnderSprite(px, py)) {
|
||||
same = false;
|
||||
}
|
||||
}
|
||||
if (!same) {
|
||||
first = k;
|
||||
break;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#define ST_SIM_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
|
|
|
|||
87
scripts/run-x68000.sh
Executable file
|
|
@ -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 >/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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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_; \
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
48
tools/scanPtrBug.py
Executable file
|
|
@ -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 <dp>` (the
|
||||
# incoming pointer arrives as offset-in-A, bank-in-X). Flagging a
|
||||
# `lda <bankdp> ... 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)
|
||||
198
tools/xdftool.py
|
|
@ -20,6 +20,8 @@ Usage:
|
|||
xdftool.py list <image>
|
||||
xdftool.py add <image> <hostfile> [name]
|
||||
xdftool.py delete <image> <name>
|
||||
xdftool.py deltree <image> <name>
|
||||
xdftool.py empty <image>
|
||||
xdftool.py extract <image> <name> <hostfile>
|
||||
xdftool.py free <image>
|
||||
"""
|
||||
|
|
@ -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("<H", e[26:28])[0]
|
||||
if e[11] & ATTR_DIR:
|
||||
self._free_subtree(cluster)
|
||||
self.free_chain(cluster)
|
||||
self.data[off] = DELETED_MARKER
|
||||
return True
|
||||
return False
|
||||
|
||||
def empty_root(self):
|
||||
# Strip the image back to a formatted blank disk, volume label kept.
|
||||
# A data floppy is minted this way rather than by FORMAT.X in an
|
||||
# emulator: copy the Human68k template and empty it, so the BPB and
|
||||
# the boot sector stay exactly as Human68k wrote them.
|
||||
names = []
|
||||
for _, _, e in self._entries():
|
||||
if e[0] == FREE_MARKER:
|
||||
break
|
||||
if e[0] == DELETED_MARKER or (e[11] & ATTR_VOLUME):
|
||||
continue
|
||||
stem = e[0:8].decode("ascii", "replace").rstrip()
|
||||
ext = e[8:11].decode("ascii", "replace").rstrip()
|
||||
more = e[12:22].split(b"\x00", 1)[0].decode("ascii", "replace").rstrip()
|
||||
names.append(f"{stem}{more}.{ext}" if ext else f"{stem}{more}")
|
||||
for n in names:
|
||||
self.delete_tree(n)
|
||||
return len(names)
|
||||
|
||||
def _free_subtree(self, cluster):
|
||||
# Free every file and nested directory reachable from a directory's
|
||||
# cluster chain. "." and ".." point back up (and at the parent), so
|
||||
# skip them or the walk eats the tree it came from.
|
||||
for _, off, e in self._entries_in_dir(cluster):
|
||||
if e[0] == FREE_MARKER:
|
||||
break
|
||||
if e[0] == DELETED_MARKER or (e[11] & ATTR_VOLUME):
|
||||
continue
|
||||
stem = bytes(e[0:11]).rstrip(b" ")
|
||||
if stem in (b".", b".."):
|
||||
continue
|
||||
child = struct.unpack("<H", e[26:28])[0]
|
||||
if e[11] & ATTR_DIR:
|
||||
self._free_subtree(child)
|
||||
self.free_chain(child)
|
||||
self.data[off] = DELETED_MARKER
|
||||
|
||||
def _alloc_write_payload(self, payload):
|
||||
# Allocate a FAT chain for `payload`, write it (zero-padding the final cluster so stale bytes never
|
||||
# leak), and return the head cluster (0 for an empty payload). Shared by add_file and
|
||||
|
|
@ -240,6 +332,37 @@ class Xdf:
|
|||
self.fat_set(cluster, 0xFFF if idx == need - 1 else chain[idx + 1])
|
||||
return chain[0]
|
||||
|
||||
def _entries_in_dir(self, cluster):
|
||||
# Yield (index, offset, entry) for any directory table: the FIXED root table when cluster is
|
||||
# 0 (FAT12 roots are not a cluster chain), else the subdirectory's chain.
|
||||
if cluster == 0:
|
||||
for i, off, e in self._entries():
|
||||
yield i, off, e
|
||||
return
|
||||
per = self.cluster_bytes // DIR_ENTRY_SIZE
|
||||
idx = 0
|
||||
guard = 0
|
||||
while 2 <= cluster < EOC_MIN:
|
||||
base = self.cluster_offset(cluster)
|
||||
for i in range(per):
|
||||
off = base + i * DIR_ENTRY_SIZE
|
||||
yield idx, off, self.data[off:off + DIR_ENTRY_SIZE]
|
||||
idx += 1
|
||||
cluster = self.fat_get(cluster)
|
||||
guard += 1
|
||||
if guard > 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("<I", entry, 28, size)
|
||||
return entry
|
||||
|
||||
def dir_cluster(self, path, create=False):
|
||||
# Resolve a directory PATH ("DATA", "DATA/LEVELS", ...) to its first cluster, optionally
|
||||
# creating each missing component. Returns None when a component is missing and create is
|
||||
# False. Root is cluster 0, which _put_entry_in_dir treats as the root table. Nesting
|
||||
# matters because jlDataOpen prefixes "DATA/", so an asset named "levels/x.dat" lands two
|
||||
# levels down -- every JoeyLib app with assets in a subdirectory needs this, not just one.
|
||||
cluster = 0
|
||||
for part in [p for p in path.upper().split("/") if p]:
|
||||
if len(part) > 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("<H", e[26:28])[0]
|
||||
break
|
||||
if found is not None:
|
||||
cluster = found
|
||||
continue
|
||||
if not create:
|
||||
return None
|
||||
free = self.free_clusters()
|
||||
if not free:
|
||||
raise OSError(f"{self.path}: no free cluster for subdirectory")
|
||||
new_cluster = free[0]
|
||||
self.fat_set(new_cluster, 0xFFF)
|
||||
cb = self.cluster_bytes
|
||||
doff = self.cluster_offset(new_cluster)
|
||||
self.data[doff:doff + cb] = bytes(cb)
|
||||
self.data[doff:doff + 32] = self._dir_entry(b". ", ATTR_DIR, new_cluster, 0)
|
||||
self.data[doff + 32:doff + 64] = self._dir_entry(b".. ", ATTR_DIR, cluster, 0)
|
||||
self._put_entry_any(cluster, self._dir_entry(stem, ATTR_DIR, new_cluster, 0))
|
||||
cluster = new_cluster
|
||||
return cluster
|
||||
|
||||
def add_file_at_path(self, path, payload):
|
||||
# "DATA/LEVELS/LEVEL01.DAT" -> 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/<blob> 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 <image> <name>", 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 <image>", 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 <image> <name> <hostfile>", file=sys.stderr)
|
||||
|
|
|
|||