LOTS of optimizations for the IIgs.

This commit is contained in:
Scott Duensing 2026-04-30 01:27:17 -05:00
parent ea1e853d5d
commit af366e7e81
40 changed files with 6864 additions and 361 deletions

View file

@ -103,7 +103,7 @@ static void initialPaint(SurfaceT *screen, bool audioOk) {
surfaceClear(screen, COLOR_BG); surfaceClear(screen, COLOR_BG);
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
audioOk ? COLOR_HINT : COLOR_BG); audioOk ? COLOR_HINT : COLOR_BG);
surfacePresent(screen); stagePresent();
} }
@ -128,9 +128,9 @@ int main(void) {
return 1; return 1;
} }
screen = surfaceGetScreen(); screen = stageGet();
if (screen == NULL) { if (screen == NULL) {
fprintf(stderr, "surfaceGetScreen returned NULL\n"); fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown(); joeyShutdown();
return 1; return 1;
} }
@ -171,11 +171,11 @@ int main(void) {
if (flashFrames > 0) { if (flashFrames > 0) {
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR); fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
surfacePresentRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H); stagePresentRect(BAR_X, BAR_Y, BAR_W, BAR_H);
flashFrames--; flashFrames--;
if (flashFrames == 0) { if (flashFrames == 0) {
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT); fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
surfacePresentRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H); stagePresentRect(BAR_X, BAR_Y, BAR_W, BAR_H);
} }
} }
} }

277
examples/draw/draw.c Normal file
View file

@ -0,0 +1,277 @@
// Drawing primitive smoke test. Lays out a 2x2 grid of cells, each
// exercising one family of primitives. On screen each cell should show
// a clear visual signal that the underlying inner loops (C or ASM)
// produced the expected pixel pattern.
//
// TL: drawPixel + drawLine (8-octant fan from cell center; pixel
// row of all 16 colors along the cell's bottom edge).
// TR: drawRect + fillRect (concentric outlines + filled blocks at
// deliberately odd x / odd width to catch nibble-edge bugs).
// BL: drawCircle + fillCircle (concentric outlines + a small filled
// disk at center).
// BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill.
//
// Runs in HOST_MODE_TAKEOVER and holds the frame until the user
// presses ESC / RETURN / SPACE.
#include <stdio.h>
#include <joey/joey.h>
#define CELL_W 160
#define CELL_H 100
// Color slots (palette 0). Color 0 is the library-forced black.
#define C_BG 0
#define C_BORDER 1 // white
#define C_RED 2
#define C_GREEN 3
#define C_BLUE 4
#define C_YELLOW 5
#define C_CYAN 6
#define C_MAGENTA 7
#define C_ORANGE 8
#define C_GRAY 9
static void buildPalette(SurfaceT *screen);
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy);
static void drawAllCellBorders(SurfaceT *screen);
static void drawPrimitivesPixelLine(SurfaceT *screen);
static void drawPrimitivesRect(SurfaceT *screen);
static void drawPrimitivesCircle(SurfaceT *screen);
static void drawPrimitivesTileFlood(SurfaceT *screen);
static void waitForKey(void);
static void buildPalette(SurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
colors[0] = 0x000;
colors[1] = 0xFFF; // white
colors[2] = 0xF00; // red
colors[3] = 0x0F0; // green
colors[4] = 0x00F; // blue
colors[5] = 0xFF0; // yellow
colors[6] = 0x0FF; // cyan
colors[7] = 0xF0F; // magenta
colors[8] = 0xF80; // orange
colors[9] = 0x888; // mid gray
colors[10] = 0x800;
colors[11] = 0x080;
colors[12] = 0x008;
colors[13] = 0x880;
colors[14] = 0x088;
colors[15] = 0x808;
paletteSet(screen, 0, colors);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
}
static void drawCellBorder(SurfaceT *screen, int16_t cx, int16_t cy) {
drawRect(screen, cx, cy, CELL_W, CELL_H, C_BORDER);
}
static void drawAllCellBorders(SurfaceT *screen) {
drawCellBorder(screen, 0, 0);
drawCellBorder(screen, CELL_W, 0);
drawCellBorder(screen, 0, CELL_H);
drawCellBorder(screen, CELL_W, CELL_H);
}
// Top-left cell: drawPixel + drawLine.
//
// 8 lines fan out from the cell center (80, 50). Four are diagonal
// (the new ASM Bresenham path) and four are axis-aligned (drawLine
// routes those to fillRect). A horizontal row of 14 pixels along the
// cell's bottom verifies drawPixel: each pixel uses a different color
// index so the leftmost ones at color 0 are invisible (they are bg)
// and 1..13 progress through the palette.
static void drawPrimitivesPixelLine(SurfaceT *screen) {
int16_t cx;
int16_t cy;
int16_t i;
cx = CELL_W / 2; // 80
cy = CELL_H / 2; // 50
drawLine(screen, cx, cy, cx + 70, cy, C_RED); // E (horizontal)
drawLine(screen, cx, cy, cx + 60, cy - 40, C_GREEN); // NE (diagonal)
drawLine(screen, cx, cy, cx, cy - 45, C_BLUE); // N (vertical)
drawLine(screen, cx, cy, cx - 60, cy - 40, C_YELLOW); // NW
drawLine(screen, cx, cy, cx - 70, cy, C_CYAN); // W
drawLine(screen, cx, cy, cx - 60, cy + 40, C_MAGENTA); // SW
drawLine(screen, cx, cy, cx, cy + 45, C_ORANGE); // S
drawLine(screen, cx, cy, cx + 60, cy + 40, C_GRAY); // SE
// Pixel row: 14 single-pixel writes at consecutive x to exercise
// both odd and even nibble paths.
for (i = 0; i < 14; i++) {
drawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
(uint8_t)((i + 1) & 0x0F));
}
}
// Top-right cell: drawRect + fillRect.
//
// Four nested rectangles with deliberately odd x/y/w/h to exercise
// the partial-byte (nibble) edge handling in halFastFillRect. The
// outermost is filled, the next outline-only, then filled with odd
// width, then a 1-pixel-wide vertical bar (drawRect collapses to a
// line via fillRect's 1-wide path).
static void drawPrimitivesRect(SurfaceT *screen) {
int16_t ox;
ox = CELL_W; // cell origin x
// Outer fill, even-aligned.
fillRect(screen, ox + 8, 8, 144, 84, C_RED);
// Inner outline, odd x to test partial-nibble edges.
drawRect(screen, ox + 17, 17, 124, 64, C_YELLOW);
// Odd width fill, odd x.
fillRect(screen, ox + 25, 25, 35, 48, C_GREEN);
// 1-pixel vertical bar (degenerate rect through fillRect 1-wide path).
fillRect(screen, ox + 100, 25, 1, 48, C_BORDER);
// Odd-x odd-w narrow bar to specifically hit hasLeading + hasTrailing
// in halFastFillRect.
fillRect(screen, ox + 75, 25, 7, 48, C_CYAN);
}
// Bottom-left cell: drawCircle + fillCircle.
//
// Concentric outlines at decreasing radii, alternating colors, plus a
// small filled disk at the center. Center is at the cell midpoint.
static void drawPrimitivesCircle(SurfaceT *screen) {
int16_t cx;
int16_t cy;
cx = CELL_W / 2;
cy = CELL_H + CELL_H / 2;
drawCircle(screen, cx, cy, 45, C_BORDER);
drawCircle(screen, cx, cy, 35, C_GREEN);
drawCircle(screen, cx, cy, 25, C_YELLOW);
drawCircle(screen, cx, cy, 15, C_CYAN);
fillCircle(screen, cx, cy, 8, C_MAGENTA);
}
// Bottom-right cell: tile + flood fill.
//
// Top portion: a 16x16 colored block, then tileSnap one of its 8x8
// quadrants and tilePaste the captured tile to a neighbor block;
// also tileCopy the same source quadrant to a third location to
// exercise the full surface-to-surface path. Then a tileCopyMasked
// case: paint a 2x1 (16x8) "stripe" containing a transparent color 0
// pattern interleaved with color, paste it over a solid backdrop with
// transparent=0; the backdrop should show through the transparent
// nibbles.
//
// Bottom portion: drawRect outlines a closed region, floodFillBounded
// fills its interior with a different color, stopping at the outline.
static void drawPrimitivesTileFlood(SurfaceT *screen) {
int16_t ox;
int16_t oy;
int16_t bx;
int16_t by;
int16_t i;
int16_t px;
TileT snapBuf;
ox = CELL_W; // 160
oy = CELL_H; // 100
// Source 16x16 block at (168, 108): a 4-quadrant pattern.
fillRect(screen, ox + 8, oy + 8, 8, 8, C_RED);
fillRect(screen, ox + 16, oy + 8, 8, 8, C_GREEN);
fillRect(screen, ox + 8, oy + 16, 8, 8, C_BLUE);
fillRect(screen, ox + 16, oy + 16, 8, 8, C_YELLOW);
// tileSnap the top-left red quadrant (block bx=21, by=13) and
// tilePaste it next to the 16x16 source as the 5th quadrant.
bx = (int16_t)((ox + 8) / 8);
by = (int16_t)((oy + 8) / 8);
tileSnap(screen, (uint8_t)bx, (uint8_t)by, &snapBuf);
tilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
// tileCopy from the green quadrant onto a fresh location below.
tileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
screen, (uint8_t)(bx + 1), (uint8_t)by);
// tileCopyMasked test: build a "transparent" striped pattern at
// (208, 132). The tile's source has color 0 in alternating
// nibbles. Paste it onto a solid orange backdrop so transparent
// nibbles let the orange show through.
fillRect(screen, ox + 80, oy + 32, 16, 8, C_ORANGE); // backdrop
// Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
for (i = 0; i < 8; i++) {
for (px = 0; px < 16; px++) {
drawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
(uint8_t)((px & 1) ? C_MAGENTA : 0));
}
}
// tileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
// -> dst at backdrop block (ox+80)/8 = 30..31, by 16
tileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
0);
// Flood-fill region: a small bordered rectangle in the cell's
// lower portion. Outline drawn in C_BORDER; floodFillBounded
// from a point inside should fill with C_CYAN, stopping at the
// border.
drawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
floodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
C_CYAN, C_BORDER);
// Plain floodFill: solid block then re-fill to a new color.
fillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
floodFill(screen, (int16_t)(ox + 120), (int16_t)(oy + 76), C_GRAY);
}
static void waitForKey(void) {
joeyWaitForAnyKey();
}
int main(void) {
JoeyConfigT config;
SurfaceT *screen;
config.hostMode = HOST_MODE_TAKEOVER;
config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64 * 1024;
config.assetBytes = 128 * 1024;
if (!joeyInit(&config)) {
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
return 1;
}
screen = stageGet();
if (screen == NULL) {
fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown();
return 1;
}
buildPalette(screen);
surfaceClear(screen, C_BG);
drawAllCellBorders(screen);
drawPrimitivesPixelLine(screen);
drawPrimitivesRect(screen);
drawPrimitivesCircle(screen);
drawPrimitivesTileFlood(screen);
stagePresent();
waitForKey();
joeyShutdown();
return 0;
}

View file

@ -81,7 +81,7 @@ static void buildPalette(SurfaceT *screen) {
static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) {
fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color);
surfacePresentRect(screen, x, y, (uint16_t)w, (uint16_t)h); stagePresentRect(x, y, (uint16_t)w, (uint16_t)h);
} }
@ -143,7 +143,7 @@ static void initialPaint(SurfaceT *screen) {
gView[i].valid = false; gView[i].valid = false;
gView[i].connected = false; gView[i].connected = false;
} }
surfacePresent(screen); stagePresent();
} }
@ -226,9 +226,9 @@ int main(void) {
return 1; return 1;
} }
screen = surfaceGetScreen(); screen = stageGet();
if (screen == NULL) { if (screen == NULL) {
fprintf(stderr, "surfaceGetScreen returned NULL\n"); fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown(); joeyShutdown();
return 1; return 1;
} }

View file

@ -149,7 +149,7 @@ static void initialPaint(SurfaceT *screen) {
gCellLit[row][col] = false; gCellLit[row][col] = false;
} }
} }
surfacePresent(screen); stagePresent();
} }
@ -174,7 +174,7 @@ static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cur
drawCell(screen, col, row, lit); drawCell(screen, col, row, lit);
x = (int16_t)(MARGIN_X + col * (CELL_W + GAP)); x = (int16_t)(MARGIN_X + col * (CELL_W + GAP));
y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP)); y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP));
surfacePresentRect(screen, x, y, CELL_W, CELL_H); stagePresentRect(x, y, CELL_W, CELL_H);
gCellLit[row][col] = lit; gCellLit[row][col] = lit;
} }
} }
@ -195,19 +195,19 @@ static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow)
if (gLastCursorX != mouseX || gLastCursorY != mouseY) { if (gLastCursorX != mouseX || gLastCursorY != mouseY) {
if (gLastCursorCol != CELL_NONE) { if (gLastCursorCol != CELL_NONE) {
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]); drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
surfacePresentRect(screen, stagePresentRect(
(int16_t)(MARGIN_X + gLastCursorCol * (CELL_W + GAP)), (int16_t)(MARGIN_X + gLastCursorCol * (CELL_W + GAP)),
(int16_t)(MARGIN_Y + gLastCursorRow * (CELL_H + GAP)), (int16_t)(MARGIN_Y + gLastCursorRow * (CELL_H + GAP)),
CELL_W, CELL_H); CELL_W, CELL_H);
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) { } else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
// Old cursor was in a gap region. Stamp background over it. // Old cursor was in a gap region. Stamp background over it.
fillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND); fillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
surfacePresentRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H); stagePresentRect(gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H);
} }
} }
drawCursor(screen, mouseX, mouseY); drawCursor(screen, mouseX, mouseY);
surfacePresentRect(screen, mouseX, mouseY, CURSOR_W, CURSOR_H); stagePresentRect(mouseX, mouseY, CURSOR_W, CURSOR_H);
gLastCursorX = mouseX; gLastCursorX = mouseX;
gLastCursorY = mouseY; gLastCursorY = mouseY;
@ -233,9 +233,9 @@ int main(void) {
return 1; return 1;
} }
screen = surfaceGetScreen(); screen = stageGet();
if (screen == NULL) { if (screen == NULL) {
fprintf(stderr, "surfaceGetScreen returned NULL\n"); fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown(); joeyShutdown();
return 1; return 1;
} }

View file

@ -8,7 +8,6 @@
// library contract, so the leftmost stripe is black in every band. // library contract, so the leftmost stripe is black in every band.
#include <stdio.h> #include <stdio.h>
#include <time.h>
#include <joey/joey.h> #include <joey/joey.h>
@ -16,13 +15,11 @@
#define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT) #define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT)
#define STRIPE_COUNT 16 #define STRIPE_COUNT 16
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT) #define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
#define DISPLAY_SECONDS 5
static void buildPalettes(SurfaceT *screen); static void buildPalettes(SurfaceT *screen);
static void buildScbs(SurfaceT *screen); static void buildScbs(SurfaceT *screen);
static void drawStripes(SurfaceT *screen); static void drawStripes(SurfaceT *screen);
static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn); static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
static void waitSeconds(int seconds);
static void buildPalettes(SurfaceT *screen) { static void buildPalettes(SurfaceT *screen) {
@ -102,15 +99,6 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
} }
static void waitSeconds(int seconds) {
time_t start;
time_t now;
start = time(NULL);
do {
now = time(NULL);
} while ((long)(now - start) < (long)seconds);
}
int main(void) { int main(void) {
@ -128,9 +116,9 @@ int main(void) {
return 1; return 1;
} }
screen = surfaceGetScreen(); screen = stageGet();
if (screen == NULL) { if (screen == NULL) {
fprintf(stderr, "surfaceGetScreen returned NULL\n"); fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown(); joeyShutdown();
return 1; return 1;
} }
@ -138,9 +126,9 @@ int main(void) {
buildPalettes(screen); buildPalettes(screen);
buildScbs(screen); buildScbs(screen);
drawStripes(screen); drawStripes(screen);
surfacePresent(screen); stagePresent();
waitSeconds(DISPLAY_SECONDS); joeyWaitForAnyKey();
joeyShutdown(); joeyShutdown();
return 0; return 0;

View file

@ -14,7 +14,6 @@
#define BALL_TILES_X (BALL_W / 8) #define BALL_TILES_X (BALL_W / 8)
#define BALL_TILES_Y (BALL_H / 8) #define BALL_TILES_Y (BALL_H / 8)
#define TILE_BYTES 32
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES) #define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES)
// SaveUnder must store rounded-up byte boundaries: x rounded down to // SaveUnder must store rounded-up byte boundaries: x rounded down to
// even, width rounded up to even. Worst case for BALL_W=16 (already // even, width rounded up to even. Worst case for BALL_W=16 (already
@ -122,9 +121,9 @@ int main(void) {
return 1; return 1;
} }
screen = surfaceGetScreen(); screen = stageGet();
if (screen == NULL) { if (screen == NULL) {
fprintf(stderr, "surfaceGetScreen returned NULL\n"); fprintf(stderr, "stageGet returned NULL\n");
joeyShutdown(); joeyShutdown();
return 1; return 1;
} }
@ -145,7 +144,7 @@ int main(void) {
buildPalette(screen); buildPalette(screen);
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX); scbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX);
surfaceClear(screen, COLOR_BG); surfaceClear(screen, COLOR_BG);
surfacePresent(screen); stagePresent();
backup.bytes = gBallBackup; backup.bytes = gBallBackup;
@ -157,7 +156,7 @@ int main(void) {
spriteSaveUnder(screen, ball, x, y, &backup); spriteSaveUnder(screen, ball, x, y, &backup);
spriteDraw(screen, ball, x, y); spriteDraw(screen, ball, x, y);
surfacePresentRect(screen, backup.x, backup.y, backup.width, backup.height); stagePresentRect(backup.x, backup.y, backup.width, backup.height);
haveBackup = true; haveBackup = true;
for (;;) { for (;;) {
@ -168,7 +167,7 @@ int main(void) {
// Stash the prior ball's region before restoring the bytes // Stash the prior ball's region before restoring the bytes
// under it. Do all off-screen work (restore + move + draw) // under it. Do all off-screen work (restore + move + draw)
// first, then waitVBL + ONE surfacePresentRect covering both // first, then waitVBL + ONE stagePresentRect covering both
// old and new regions. Putting waitVBL immediately before the // old and new regions. Putting waitVBL immediately before the
// present lets the present land inside the VBL window so the // present lets the present land inside the VBL window so the
// CRT never sees a half-updated framebuffer (matters most on // CRT never sees a half-updated framebuffer (matters most on
@ -206,7 +205,7 @@ int main(void) {
: (backup.y + backup.height)); : (backup.y + backup.height));
joeyWaitVBL(); joeyWaitVBL();
surfacePresentRect(screen, unionX, unionY, stagePresentRect(unionX, unionY,
(uint16_t)(unionRight - unionX), (uint16_t)(unionRight - unionX),
(uint16_t)(unionBottom - unionY)); (uint16_t)(unionBottom - unionY));
haveBackup = true; haveBackup = true;

View file

@ -23,9 +23,40 @@ void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
// Read a pixel value. Off-surface coordinates return 0. // Read a pixel value. Off-surface coordinates return 0.
uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y); uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y);
// Plot a line from (x0, y0) to (x1, y1) using Bresenham. Endpoints
// are inclusive. Off-surface pixels are skipped per-pixel; lines that
// pass entirely off-surface draw nothing. Horizontal and vertical
// runs hit fast paths.
void drawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex);
// Outline a rectangle (1-pixel-wide border). 1xN / Nx1 degenerate
// to vertical / horizontal lines; 1x1 to a single pixel. Negative or
// zero dimensions are no-ops.
void drawRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex);
// Fill a solid rectangle. Negative or zero dimensions are no-ops. // Fill a solid rectangle. Negative or zero dimensions are no-ops.
void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex);
// Outline a circle of radius r centered at (cx, cy) using Bresenham
// midpoint. r == 0 plots a single pixel.
void drawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
// Fill a disk of radius r centered at (cx, cy). r == 0 plots a single
// pixel. Spans are emitted per scanline using midpoint symmetry.
void fillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
// Flood fill a 4-connected region starting at (x, y). Replaces every
// pixel of the original color reached via N/S/E/W steps. No-op if
// (x, y) is off-surface or already matches newColor.
void floodFill(SurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
// Flood fill a 4-connected region starting at (x, y), stopping at
// boundary pixels of color boundaryColor. Replaces every reachable
// pixel that is not boundaryColor with newColor. Used for vector-art
// rendering (e.g. Sierra-style picture playback): outline a closed
// region with drawLine in boundaryColor, then fill with this.
void floodFillBounded(SurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor);
// Blit an asset onto the surface at (x, y). Source nibbles overwrite // Blit an asset onto the surface at (x, y). Source nibbles overwrite
// destination nibbles verbatim -- the caller is responsible for // destination nibbles verbatim -- the caller is responsible for
// matching the asset's palette to the destination palette (typically // matching the asset's palette to the destination palette (typically

View file

@ -89,6 +89,12 @@ typedef enum {
void joeyInputPoll(void); void joeyInputPoll(void);
// Block until the user presses any key. Internally polls via
// joeyInputPoll, so per-port halInputPoll machinery (including
// audio-friendly IRQ-driven samplers) keeps working while the
// wait loop runs.
void joeyWaitForAnyKey(void);
bool joeyKeyDown(JoeyKeyE key); bool joeyKeyDown(JoeyKeyE key);
bool joeyKeyPressed(JoeyKeyE key); bool joeyKeyPressed(JoeyKeyE key);
bool joeyKeyReleased(JoeyKeyE key); bool joeyKeyReleased(JoeyKeyE key);

View file

@ -13,6 +13,7 @@
#include "palette.h" #include "palette.h"
#include "asset.h" #include "asset.h"
#include "draw.h" #include "draw.h"
#include "tile.h"
#include "present.h" #include "present.h"
#include "input.h" #include "input.h"
#include "audio.h" #include "audio.h"

View file

@ -63,6 +63,20 @@
#define JOEYLIB_PLATFORM_NAME "MS-DOS" #define JOEYLIB_PLATFORM_NAME "MS-DOS"
#endif #endif
// ----- ORCA-C named load segments -----
//
// On the IIgs the ORCA Linker fits each load segment in its own bank,
// so spilling cross-platform .c files into named segments is the way
// to keep monolithic IIgs binaries under the 64 KB-per-bank _ROOT
// limit. The `segment "name";` statement is ORCA-C-specific syntax
// (see ORCA/C ch. 30); other ports' compilers don't recognize it, so
// the macro evaluates to nothing on Amiga/ST/DOS.
#ifdef JOEYLIB_PLATFORM_IIGS
#define JOEYLIB_SEGMENT(name) segment name;
#else
#define JOEYLIB_SEGMENT(name)
#endif
// ----- Library version ----- // ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1 #define JOEYLIB_VERSION_MAJOR 1

View file

@ -1,9 +1,11 @@
// Present / slam. // Stage present.
// //
// surfacePresent copies pixels, SCBs, and palettes from a source // stagePresent flips the library-owned stage (back-buffer) to the
// surface to the visible display. On chunky platforms (IIgs, DOS) this // display. On chunky platforms (IIgs, DOS) this is a direct copy; on
// is a direct copy; on planar platforms (Amiga, Atari ST) this is a // planar platforms (Amiga, Atari ST) this is a chunky-to-planar
// chunky-to-planar conversion. See docs/DESIGN.md section 7. // conversion. Drawing primitives mark per-row dirty ranges on the
// stage as a side effect, so stagePresent only touches rows that
// actually changed since the last present. See docs/DESIGN.md.
#ifndef JOEYLIB_PRESENT_H #ifndef JOEYLIB_PRESENT_H
#define JOEYLIB_PRESENT_H #define JOEYLIB_PRESENT_H
@ -12,12 +14,15 @@
#include "surface.h" #include "surface.h"
#include "types.h" #include "types.h"
// Present the entire source surface to the display. // Flip the dirty regions of the stage to the display, then clear the
void surfacePresent(const SurfaceT *src); // dirty state. Cheap when nothing has changed since the last call.
void stagePresent(void);
// Present a rectangular region of the source surface to the display. // Flip a specific rectangular region of the stage to the display,
// The rect is clipped to the surface. Negative or zero dimensions are // regardless of dirty state. Coordinates are clipped to the surface;
// no-ops. // negative or zero dimensions are no-ops. Does not consult or modify
void surfacePresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h); // the dirty arrays -- callers mixing stagePresentRect with stagePresent
// in the same frame may see redundant work on the next stagePresent.
void stagePresentRect(int16_t x, int16_t y, uint16_t w, uint16_t h);
#endif #endif

View file

@ -31,13 +31,15 @@ typedef struct SurfaceT SurfaceT;
SurfaceT *surfaceCreate(void); SurfaceT *surfaceCreate(void);
// Release an offscreen surface previously returned by surfaceCreate. // Release an offscreen surface previously returned by surfaceCreate.
// Passing NULL is a no-op. Passing the screen surface is a no-op. // Passing NULL is a no-op. Passing the stage is a no-op.
void surfaceDestroy(SurfaceT *s); void surfaceDestroy(SurfaceT *s);
// The library's pre-allocated screen surface. This is the surface the // The library-owned stage: the back-buffer surface that stagePresent
// library presents to the display. Always valid between joeyInit and // flips to the display. Always valid between joeyInit and joeyShutdown.
// joeyShutdown. // On IIgs the stage's pixel buffer is pinned to bank $01 SHR space with
SurfaceT *surfaceGetScreen(void); // shadow inhibited so writes are full-speed (2.8 MHz) and isolated from
// the displayed framebuffer until the next stagePresent.
SurfaceT *stageGet(void);
// Copy pixels, SCBs, and palettes from src into dst. Both must be valid // Copy pixels, SCBs, and palettes from src into dst. Both must be valid
// surfaces. // surfaces.

96
include/joey/tile.h Normal file
View file

@ -0,0 +1,96 @@
// Tiles: 8x8 pixel blocks aligned on the 8-pixel grid of any surface.
//
// A "tile" in JoeyLib isn't a separate object -- it's just the 8x8
// region of a SurfaceT at block coordinates (bx, by), where bx is in
// [0, 39] and by is in [0, 24] (40x25 blocks per 320x200 surface).
// The tile API is a small set of operations that move 32-byte chunks
// between surfaces or fill them with a solid color.
//
// Why this shape: anything you can do with a regular surface --
// drawPixel, drawLine, fillRect, blits -- also works on tile-aligned
// regions, so authors can paint, edit, and procedurally generate tile
// content using the same primitives they use for everything else.
// "Fonts," "terrain tilesets," and "spritesheets" are just surfaces
// you treat as tile sources at draw time.
//
// Snap / paste use a small TileT value type so callers don't need a
// 32 KB scratch surface for save-under-style work.
//
// Block coords map to pixels by multiplying by 8 -- byte-aligned in
// 4bpp packed (8 px = 4 bytes per row), so all the operations are
// byte-shoveling memcpys with no shifting.
#ifndef JOEYLIB_TILE_H
#define JOEYLIB_TILE_H
#include "platform.h"
#include "surface.h"
#include "types.h"
// ----- Constants -----
#define TILE_PIXELS_PER_SIDE 8
#define TILE_BYTES_PER_ROW 4
#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE)
// Sentinel for asciiMap entries that should not draw. drawText
// advances the cursor past TILE_NO_GLYPH chars without writing.
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
// ----- Types -----
// Stack-allocated 32-byte snapshot buffer for tileSnap / tilePaste.
typedef struct TileT {
uint8_t pixels[TILE_BYTES];
} TileT;
// ----- API -----
// Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on
// dst, opaque. Out-of-range block coordinates on either side are
// silent no-ops; src and dst can be the same surface.
void tileCopy(SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy);
// Like tileCopy but pixels equal to transparentIndex are skipped --
// the destination pixel keeps its original value. Use this for fonts
// (transparentIndex = 0 leaves the page background showing through
// glyph backgrounds) and for any tileset where some pixels are meant
// to be see-through.
void tileCopyMasked(SurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const SurfaceT *src, uint8_t srcBx, uint8_t srcBy,
uint8_t transparentIndex);
// Fill the 8x8 block at (bx, by) with a solid color. Equivalent to
// fillRect(s, bx*8, by*8, 8, 8, colorIndex) but skips the rect
// clipping math since tile coords are already known to be in range.
void tileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
// Capture the 8x8 block at (bx, by) into the caller's TileT. Used
// for save-under style work where allocating a scratch surface would
// be overkill.
void tileSnap(const SurfaceT *src, uint8_t bx, uint8_t by, TileT *out);
// Paste a TileT back onto a surface at block (bx, by). Always
// opaque; use tileCopyMasked if you need transparency.
void tilePaste(SurfaceT *dst, uint8_t bx, uint8_t by, const TileT *in);
// Draw a NUL-terminated ASCII string at block (bx, by) using glyphs
// pulled from fontSurface.
//
// asciiMap is a 256-entry table mapping ASCII code to glyph location
// on fontSurface, encoded as a packed uint16_t: low byte = source
// blockX, high byte = source blockY. asciiMap[c] == TILE_NO_GLYPH
// causes that character to be skipped (cursor advances, nothing
// drawn). Glyph color 0 is treated as transparent so the underlying
// surface shows through glyph backgrounds.
//
// The cursor wraps to the next row at the right edge and truncates
// at the bottom edge.
void drawText(SurfaceT *dst, uint8_t bx, uint8_t by,
const SurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
#endif

View file

@ -60,6 +60,8 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/Hello HELLO_BIN := $(BINDIR)/Hello
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/Pattern PATTERN_BIN := $(BINDIR)/Pattern
DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/Draw
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/Keys KEYS_BIN := $(BINDIR)/Keys
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
@ -76,7 +78,7 @@ DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.PHONY: all amiga clean-amiga .PHONY: all amiga clean-amiga
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES) all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES)
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -118,6 +120,10 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
$(DRAW_BIN): $(DRAW_SRC) $(LIB)
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)

View file

@ -45,6 +45,8 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO.PRG HELLO_BIN := $(BINDIR)/HELLO.PRG
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN.PRG PATTERN_BIN := $(BINDIR)/PATTERN.PRG
DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/DRAW.PRG
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS.PRG KEYS_BIN := $(BINDIR)/KEYS.PRG
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
@ -61,7 +63,7 @@ DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.PHONY: all atarist clean-atarist .PHONY: all atarist clean-atarist
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES) all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES)
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -110,6 +112,10 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
$(DRAW_BIN): $(DRAW_SRC) $(LIB)
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)

View file

@ -39,6 +39,8 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO.EXE HELLO_BIN := $(BINDIR)/HELLO.EXE
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN.EXE PATTERN_BIN := $(BINDIR)/PATTERN.EXE
DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/DRAW.EXE
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS.EXE KEYS_BIN := $(BINDIR)/KEYS.EXE
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
@ -54,7 +56,7 @@ DATA_DIR := $(BINDIR)/DATA
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
.PHONY: all dos clean-dos .PHONY: all dos clean-dos
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES) all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(DATA_FILES)
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -94,6 +96,11 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
$(DOS_EMBED_DPMI) $@ $(DOS_EMBED_DPMI) $@
$(DRAW_BIN): $(DRAW_SRC) $(LIB)
@mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
$(DOS_EMBED_DPMI) $@
$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@

View file

@ -13,16 +13,20 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM)
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c) PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c)
# Hand-rolled .asm sources go through ORCA's macro assembler via
# iix-build.sh's `assemble` dispatch. Each .asm declares its target
# load segment in the START operand (e.g. peislam.asm -> PEISLAMS)
# so the linker places its bytes in a separate bank from _ROOT.
# See ORCA/M for IIgs ch. 6 "Load Segments" for the mechanism.
PORT_ASM_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.asm)
# audio.c is the no-op stub linked into every demo. audio_full.c is the # audio_full.c declares its functions in the AUDIOIMPL load segment
# real implementation (NewHandle / fopen / JSL trampoline) and links # (`segment "AUDIOIMPL"` at file scope, see ORCA/C ch. 30) so the
# only into AUDIO -- the IIgs build is monolithic, so pulling Memory # implementation code lives in its own bank, not _ROOT. That lets
# Manager + ORCA stdio into every binary blows the linker's # the same source link into every binary, replacing the earlier
# "Expression too complex" budget. The two files define the same # audio.c-stub vs audio_full.c-real split. The 34 KB NTP replayer
# halAudio* symbols; iigs/audio.c is filtered out of the AUDIO source # bytes still ride along via the xxd-baked header.
# set, audio_full.c is filtered out of the everyone-else set. PORT_C_SRCS := $(PORT_C_SRCS_ALL)
PORT_C_SRCS := $(filter-out %/audio_full.c, $(PORT_C_SRCS_ALL))
PORT_C_SRCS_AUDIO := $(filter-out %/audio.c, $(PORT_C_SRCS_ALL))
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that # IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter # DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter
@ -34,9 +38,6 @@ CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \ CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \
$(REPO_DIR)/src/codegen/spriteCompile.c $(REPO_DIR)/src/codegen/spriteCompile.c
LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
LIB_SRCS_AUDIO := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS_AUDIO) $(CODEGEN_SRCS)
# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged # NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged
# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB # source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB
# raw 65816 binary that the IIgs audio HAL loads at runtime via # raw 65816 binary that the IIgs audio HAL loads at runtime via
@ -45,13 +46,17 @@ LIB_SRCS_AUDIO := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS_AUDIO) $(CODEGEN_SRCS)
# load address even though it was assembled with `org $0F0000`. # load address even though it was assembled with `org $0F0000`.
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_BIN := $(BUILD)/audio/ntpplayer.bin
NTP_HEADER := $(BUILD)/audio/ntpplayer_data.h NTP_ASM := $(BUILD)/audio/ntpdata.asm
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32 IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(PORT_ASM_SRCS_ALL) $(NTP_ASM) $(CODEGEN_SRCS)
HELLO_SRC := $(EXAMPLES)/hello/hello.c HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO HELLO_BIN := $(BINDIR)/HELLO
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN PATTERN_BIN := $(BINDIR)/PATTERN
DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/DRAW
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS KEYS_BIN := $(BINDIR)/KEYS
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
@ -77,7 +82,11 @@ IIX_INCLUDES := \
-I $(REPO_DIR)/src/codegen -I $(REPO_DIR)/src/codegen
.PHONY: all iigs iigs-disk clean-iigs .PHONY: all iigs iigs-disk clean-iigs
all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) # Building the disk implicitly builds every binary it depends on, so
# `make iigs` ends with a fresh joey.2mg on every change. Without this,
# stale disk images would silently mask binary updates -- a surprise
# when the run script always boots from joey.2mg.
all iigs: $(DISK_IMG)
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN) $(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -85,51 +94,55 @@ $(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@ mv $(BUILD)/audio/ntpplayer $@
# Bake the NTP replayer bytes into a C header so audio_full.c can link # Bake the NTP replayer bytes into an ORCA-M asm file. The asm declares
# the player into the AUDIO binary instead of fopen'ing a separate # the bytes in a `data NTPDATA` segment; ORCA's linker groups same-
# NTPPLAYER.BIN at runtime. NTP is bank-internal / PIC, so the linked # name object segments into one load segment, and the GS/OS loader
# bytes still BlockMove cleanly into the Memory Manager handle the HAL # places it in its own bank. Net effect: the 34 KB of NTP bytes don't
# allocates. Same xxd-i pattern as test_assets.h. # crowd _ROOT in any binary, so audio_full.c can link into every demo
$(NTP_HEADER): $(NTP_BIN) # (vs the old audio.c-stub split). audio_full.c references
# gNtpPlayerBytes / gNtpPlayerBytes_len as externs (case-sensitive
# symbol match against the asm labels).
$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@echo "// Generated by make/iigs.mk -- NinjaTrackerPlus replayer bytes." > $@ $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh $(NTP_BIN) $@ NTPDATA gNtpPlayerBytes gNtpPlayerBytes_len
@echo "#ifndef JOEYLIB_NTPPLAYER_DATA_H" >> $@
@echo "#define JOEYLIB_NTPPLAYER_DATA_H" >> $@
@printf "static const unsigned char gNtpPlayerBytes[] = {\n" >> $@
@xxd -i < $(NTP_BIN) >> $@
@printf "};\nstatic const unsigned int gNtpPlayerBytes_len = %d;\n" $$(wc -c < $(NTP_BIN)) >> $@
@echo "#endif" >> $@
# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with # iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
# #pragma noroot). The example source supplies main(); libjoey sources # #pragma noroot). The example source supplies main(); libjoey sources
# are the extras. The chtyp post-step tags the output as GS/OS S16 # are the extras. The chtyp post-step tags the output as GS/OS S16
# ($B3) so GS/OS recognizes it as launchable; the file-type lives in # ($B3) so GS/OS recognizes it as launchable; the file-type lives in
# a user.com.apple.FinderInfo xattr that iix and profuse preserve. # a user.com.apple.FinderInfo xattr that iix and profuse preserve.
$(HELLO_BIN): $(HELLO_SRC) $(LIB_SRCS) $(IIGS_BUILD) #
# All binaries use ORCA-C large memory model (-b). Cost: slightly
# larger / slower compiled C per the ORCA docs. Win: 32-bit pointers
# everywhere, so library asm can take SurfaceT* args via one
# consistent ABI (small-mm 16-bit pointers truncated bank bytes,
# which broke any asm that wanted to address bank-1 stage memory).
$(HELLO_BIN): $(HELLO_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(HELLO_SRC) $(LIB_SRCS) $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(HELLO_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_BUILD) $(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(IIGS_BUILD) $(DRAW_BIN): $(DRAW_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS) $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(IIGS_BUILD) $(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS) $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
# Sprite demo uses ORCA-C large memory model (-b) so pointers are $(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
# 32-bit and the codegen-arena JSL stub can call cross-bank into the @mkdir -p $(dir $@)
# arena. Without -b, ORCA-C's 16-bit pointers would lose the bank $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
# byte and the stub would JSL into bank 0 (system memory) -> crash. $(IIGS_IIX) chtyp -t S16 $@
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_BUILD)
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_BUILD) -b $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
@ -152,18 +165,18 @@ $(info iigs: php-cli not installed -- AUDIO demo will ship without TEST.NTP; ins
AUDIO_DATA_FILES := $(AUDIO_SFX) AUDIO_DATA_FILES := $(AUDIO_SFX)
endif endif
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS_AUDIO) $(NTP_HEADER) $(IIGS_BUILD) $(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_BUILD) -b $(IIX_INCLUDES) -I $(dir $(NTP_HEADER)) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS_AUDIO) $(IIGS_BUILD) -b $(IIX_INCLUDES) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(IIGS_IIX) chtyp -t S16 $@
# Assemble an 800KB ProDOS 2img containing the examples, ready to # Assemble an 800KB ProDOS 2img containing the examples, ready to
# mount in GSplus alongside a GS/OS boot volume. # mount in GSplus alongside a GS/OS boot volume.
iigs-disk: $(DISK_IMG) iigs-disk: $(DISK_IMG)
$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(AUDIO_DATA_FILES) $(IIGS_PACKAGE) $(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(AUDIO_DATA_FILES) $(IIGS_PACKAGE)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) -- $(AUDIO_DATA_FILES) $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) -- $(AUDIO_DATA_FILES)
clean-iigs: clean-iigs:
rm -rf $(BUILD) rm -rf $(BUILD)

View file

@ -17,27 +17,31 @@
# #
# scripts/run-amiga.sh # runs Pattern # scripts/run-amiga.sh # runs Pattern
# scripts/run-amiga.sh hello # runs Hello # scripts/run-amiga.sh hello # runs Hello
# scripts/run-amiga.sh keys # runs Keys # scripts/run-amiga.sh draw # runs Draw
#
# Argument is any built example name (case-insensitive); the script
# normalizes it to PascalCase (first letter upper, rest lower) which
# is the JoeyLib convention for Amiga binary filenames.
set -euo pipefail set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern} prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/amiga/bin bin_dir=$repo/build/amiga/bin
support=$repo/toolchains/emulators/support support=$repo/toolchains/emulators/support
case $prog in prog_lower=${prog,,}
hello) file=Hello ;; file=${prog_lower^}
pattern) file=Pattern ;;
keys) file=Keys ;;
joy) file=Joy ;;
sprite) file=Sprite ;;
audio) file=Audio ;;
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
esac
if [[ ! -f "$bin_dir/$file" ]]; then if [[ ! -f "$bin_dir/$file" ]]; then
echo "$bin_dir/$file not built. Run 'make amiga' first." >&2 echo "$bin_dir/$file not built. Run 'make amiga' first." >&2
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -executable -printf '%f\n' >&2 2>/dev/null || true
exit 1 exit 1
fi fi
@ -56,12 +60,9 @@ dump_keep=/tmp/joeylib-amiga-dump
trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf "$work"' EXIT trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf "$work"' EXIT
mkdir -p "$work/s" mkdir -p "$work/s"
cp "$bin_dir/Hello" "$work/" 2>/dev/null || true # Stage every built binary (executable file at top of bin_dir, no
cp "$bin_dir/Pattern" "$work/" 2>/dev/null || true # extension on Amiga). DATA/ is copied separately below.
cp "$bin_dir/Keys" "$work/" 2>/dev/null || true find "$bin_dir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
cp "$bin_dir/Joy" "$work/" 2>/dev/null || true
cp "$bin_dir/Sprite" "$work/" 2>/dev/null || true
cp "$bin_dir/Audio" "$work/" 2>/dev/null || true
# Stage the DATA folder (test.mod, test.sfx) the audio demo loads from # Stage the DATA folder (test.mod, test.sfx) the audio demo loads from
# the boot volume at runtime. # the boot volume at runtime.
if [[ -d "$bin_dir/DATA" ]]; then if [[ -d "$bin_dir/DATA" ]]; then

View file

@ -6,28 +6,31 @@
# #
# scripts/run-atarist.sh # runs PATTERN.PRG # scripts/run-atarist.sh # runs PATTERN.PRG
# scripts/run-atarist.sh hello # runs HELLO.PRG # scripts/run-atarist.sh hello # runs HELLO.PRG
# scripts/run-atarist.sh keys # runs KEYS.PRG # scripts/run-atarist.sh draw # runs DRAW.PRG
#
# Argument is any built example name (case-insensitive); the script
# upper-cases it and appends .PRG, then checks the file exists.
set -euo pipefail set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern} prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/atarist/bin bin_dir=$repo/build/atarist/bin
file=${prog^^}.PRG
case $prog in
hello) file=HELLO.PRG ;;
pattern) file=PATTERN.PRG ;;
keys) file=KEYS.PRG ;;
joy) file=JOY.PRG ;;
sprite) file=SPRITE.PRG ;;
audio) file=AUDIO.PRG ;;
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
esac
tos=$repo/toolchains/emulators/support/emutos-512k.img tos=$repo/toolchains/emulators/support/emutos-512k.img
if [[ ! -f "$bin_dir/$file" ]]; then if [[ ! -f "$bin_dir/$file" ]]; then
echo "$bin_dir/$file not built. Run 'make atarist' first." >&2 echo "$bin_dir/$file not built. Run 'make atarist' first." >&2
if compgen -G "$bin_dir/*.PRG" > /dev/null; then
echo "available examples in $bin_dir:" >&2
ls "$bin_dir"/*.PRG | xargs -n1 basename >&2
fi
exit 1 exit 1
fi fi
if [[ ! -f $tos ]]; then if [[ ! -f $tos ]]; then

View file

@ -3,26 +3,29 @@
# #
# scripts/run-dos.sh # runs PATTERN # scripts/run-dos.sh # runs PATTERN
# scripts/run-dos.sh hello # runs HELLO # scripts/run-dos.sh hello # runs HELLO
# scripts/run-dos.sh keys # runs KEYS # scripts/run-dos.sh draw # runs DRAW
#
# Argument is any built example name (case-insensitive); the script
# upper-cases it and appends .EXE, then checks the file exists.
set -euo pipefail set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern} prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/dos/bin bin_dir=$repo/build/dos/bin
file=${prog^^}.EXE
case $prog in
hello) file=HELLO.EXE ;;
pattern) file=PATTERN.EXE ;;
keys) file=KEYS.EXE ;;
joy) file=JOY.EXE ;;
sprite) file=SPRITE.EXE ;;
audio) file=AUDIO.EXE ;;
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
esac
if [[ ! -f "$bin_dir/$file" ]]; then if [[ ! -f "$bin_dir/$file" ]]; then
echo "$bin_dir/$file not built. Run 'make dos' first." >&2 echo "$bin_dir/$file not built. Run 'make dos' first." >&2
if compgen -G "$bin_dir/*.EXE" > /dev/null; then
echo "available examples in $bin_dir:" >&2
ls "$bin_dir"/*.EXE | xargs -n1 basename >&2
fi
exit 1 exit 1
fi fi

View file

@ -17,16 +17,28 @@
set -euo pipefail set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern} prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
case $prog in bin_dir=$repo/build/iigs/bin
hello|pattern|keys|joy|sprite|audio) ;; target=${prog^^}
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;; if [[ ! -f "$bin_dir/$target" ]]; then
esac echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
if compgen -G "$bin_dir/*" > /dev/null; then
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -printf '%f\n' \
| grep -vE '\.2mg$|\.txt$' >&2 || true
fi
exit 1
fi
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg data_disk=$bin_dir/joey.2mg
for f in "$sys_disk" "$data_disk"; do for f in "$sys_disk" "$data_disk"; do
if [[ ! -f $f ]]; then if [[ ! -f $f ]]; then
@ -42,24 +54,30 @@ mkdir -p "$out"
cp "$sys_disk" "$work/boot.po" cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg" cp "$data_disk" "$work/joey.2mg"
# Lua script: on every CPU stop (BRK, breakpoint, watchpoint, manual # Lua script: drives Finder via natural keyboard + macadb key fields
# halt), append a state snapshot to crash.txt. This way we don't need # to launch the requested example, dumps register state on any halt
# the user to type anything at the debugger window -- whatever halts # (BRK, breakpoint, watchpoint, manual stop). Field names for keys
# the CPU lands a record in crash.txt. # come from MAME's apple2gs macadb input definitions:
cat > "$work/crash-hook.lua" <<'LUA' # :macadb:KEY0 -> "d D"
-- Crash diagnostics for IIgs demos. Auto-resumes the initial debug # :macadb:KEY1 -> "o O"
-- pause so the user doesn't need to type "go". On any subsequent halt # :macadb:KEY2 -> "j J", "p P"
-- (BRK, watchpoint, breakpoint) outside ROM, dumps registers + bytes # :macadb:KEY3 -> "Command / Open Apple"
-- around PC to crash.txt. ROM halts (PB == 0xFE/0xFF) are skipped so # Letters not on KEY0..2 fall back to natkeyboard:post() (which
-- we don't fill the file with normal IIgs ROM stack walking. # handles modifier-less character entry only).
# Type the FULL program name to disambiguate (DRAW vs DATA which both
# start with D and live in the same JOEYLIB volume).
prog_select_str=${target}
cat > "$work/crash-hook.lua" <<LUA
-- Crash diagnostics + Finder driver for IIgs demos. Auto-resumes
-- the initial debug pause; at calibrated frame counts taps "J" to
-- select JOEYLIB, Cmd-O to open it, the program first letter to
-- select the binary, Cmd-O to launch it. On any halt (BRK trap,
-- watchpoint, breakpoint) dumps registers + bytes around PC to
-- crash.txt.
local cpu = manager.machine.devices[":maincpu"] local cpu = manager.machine.devices[":maincpu"]
local prog = cpu.spaces["program"] local prog = cpu.spaces["program"]
local outpath = "/tmp/mame-iigs/crash.txt" local outpath = "/tmp/mame-iigs/crash.txt"
local function in_rom(pb)
return pb == 0xFE or pb == 0xFF
end
local function dump(label) local function dump(label)
local f = io.open(outpath, "a") local f = io.open(outpath, "a")
if f == nil then return end if f == nil then return end
@ -75,16 +93,19 @@ local function dump(label)
f:write(string.format(" %02X", b)) f:write(string.format(" %02X", b))
end end
f:write("\n") f:write("\n")
-- Probe addresses from joeyDraw.asm
f:write(string.format(" probe 012050=%02X 011F80=%02X 023000=%02X\n",
prog:read_u8(0x012050),
prog:read_u8(0x011F80),
prog:read_u8(0x023000)))
f:close() f:close()
end end
-- Lua can't reliably terminate MAME from this version's API; instead
-- write a marker file and let the bash launcher poll for it and kill
-- the process. "done" file = launcher should shut down.
local done_marker = "/tmp/mame-iigs/.done" local done_marker = "/tmp/mame-iigs/.done"
local started = false local started = false
local crashed = false local crashed = false
local boot_frames = 0 local boot_frames = 0
local function signal_done(reason) local function signal_done(reason)
local f = io.open(done_marker, "w") local f = io.open(done_marker, "w")
if f ~= nil then if f ~= nil then
@ -93,13 +114,86 @@ local function signal_done(reason)
end end
end end
local nat = manager.machine.natkeyboard
local function get_field(port, field_name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[field_name]
end
local key_cmd = get_field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function log_step(label)
local f = io.open("/tmp/mame-iigs/steps.txt", "a")
if f ~= nil then
f:write(string.format("frame=%d %s\n", boot_frames, label))
f:close()
end
end
local function snap(label)
log_step("snap " .. label)
manager.machine.video:snapshot()
end
-- Finder navigation: natkeyboard handles per-letter scancode/timing
-- correctly; the Cmd modifier is forced via the macadb field for the
-- duration of the post() call. Mirrors the proven approach in the
-- pre-existing /tmp/auto_run.lua.
local steps = {
{ 2700, function() snap("preboot") end },
{ 3000, function() snap("at_3000_finder") log_step("post J") nat:post("J") end },
{ 3120, function() snap("after_J") log_step("Cmd hold") press(key_cmd) end },
{ 3126, function() log_step("post o under Cmd") nat:post("o") end },
{ 3180, function() log_step("Cmd release") release(key_cmd) end },
{ 3300, function() snap("after_Cmd_O") end },
{ 3540, function() log_step("post name") nat:post("${prog_select_str}") end },
{ 3600, function() snap("after_first") end },
{ 3660, function() log_step("Cmd hold #2") press(key_cmd) end },
{ 3666, function() log_step("post o #2") nat:post("o") end },
{ 3720, function() log_step("Cmd release #2") release(key_cmd) end },
{ 3900, function() snap("after_launch") end },
{ 6000, function() snap("running") end },
{ 9000, function() snap("running_2") end },
{ 12000, function() snap("running_3") end },
{ 13500, function() snap("running_4") end },
{ 14500, function() snap("running_5") end },
{ 15500, function() snap("running_6") end },
{ 16500, function() snap("running_7") end },
}
local step_idx = 1
-- Probe-address poll. Watch 3 addresses and log every change, so we
-- can verify which (if any) is reachable by our long-mode STAs.
local last1, last2, last3, last4, last5, last6, last7 = -1, -1, -1, -1, -1, -1, -1
local function check_ckpt()
local v1 = prog:read_u8(0x012050)
local v2 = prog:read_u8(0x011F80)
local v3 = prog:read_u8(0x023000)
local v4 = prog:read_u8(0xE12050) -- SHR pixel byte (mirror of $012050 once blitted)
local v5 = prog:read_u8(0xE19D00) -- SCB row 0 (0 = 320 mode)
local v6 = prog:read_u8(0xE15000) -- SHR pixel byte mid-screen (row ~38, col ~64)
local v7 = prog:read_u8(0xE19E00) -- palette[0][0] low byte (color 0 lo)
if v1 ~= last1 or v2 ~= last2 or v3 ~= last3 or v4 ~= last4 or v5 ~= last5 or v6 ~= last6 or v7 ~= last7 then
local f = io.open("/tmp/mame-iigs/ckpt-trace.txt", "a")
if f ~= nil then
f:write(string.format("frame=%d 012050=%02X 011F80=%02X 023000=%02X E12050=%02X E19D00=%02X E15000=%02X E19E00=%02X\n",
boot_frames, v1, v2, v3, v4, v5, v6, v7))
f:close()
end
last1, last2, last3, last4, last5, last6, last7 = v1, v2, v3, v4, v5, v6, v7
end
end
emu.register_periodic(function() emu.register_periodic(function()
local dbg = manager.machine.debugger local dbg = manager.machine.debugger
if dbg == nil then return end if dbg == nil then return end
if dbg.execution_state == "stop" then if dbg.execution_state == "stop" then
if not started then if not started then
-- First halt is the -debug startup pause; auto-resume so
-- emulation begins without manual input.
started = true started = true
dbg.execution_state = "run" dbg.execution_state = "run"
return return
@ -111,10 +205,12 @@ emu.register_periodic(function()
end end
else else
boot_frames = boot_frames + 1 boot_frames = boot_frames + 1
-- Watchdog: ~30 wall-sec at 60 Hz. If nothing crashes by check_ckpt()
-- then, dump current state (likely the demo running fine) while step_idx <= #steps and boot_frames >= steps[step_idx][1] do
-- and tell the launcher to shut down so we can grab joeylog. steps[step_idx][2]()
if boot_frames > 1800 and not crashed then step_idx = step_idx + 1
end
if boot_frames > 18000 and not crashed then
crashed = true crashed = true
dump("watchdog") dump("watchdog")
signal_done("watchdog") signal_done("watchdog")
@ -130,7 +226,8 @@ cat <<EOF
MAME apple2gs (auto-launch ${prog^^}): MAME apple2gs (auto-launch ${prog^^}):
Boot disk: $work/boot.po (flop3) Boot disk: $work/boot.po (flop3)
GS/OS will boot directly into ${prog^^}; no Finder navigation needed. Boots GS/OS, waits ~50s for Finder, then drives the keyboard via Lua
to select the JOEYLIB volume and double-launch ${prog^^}.
On crash the MAME debugger halts and Lua dumps state to: On crash the MAME debugger halts and Lua dumps state to:
$out/crash.txt $out/crash.txt
@ -146,6 +243,11 @@ cleanup() {
if [[ -f $work/debug.log ]]; then if [[ -f $work/debug.log ]]; then
mv -f "$work/debug.log" "$out/debug.log" mv -f "$work/debug.log" "$out/debug.log"
fi fi
# Snapshots that the Lua hook captured for visual debugging.
if [[ -d $work/snap ]]; then
rm -rf "$out/snap"
mv "$work/snap" "$out/snap"
fi
local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
local mnt=$work/_mnt local mnt=$work/_mnt
if [[ -x $profuse && -f $work/joey.2mg ]]; then if [[ -x $profuse && -f $work/joey.2mg ]]; then
@ -167,11 +269,11 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
# Headless by default (-video none). Set MAME_WINDOW=1 to get a real # Visible by default. Set MAME_HEADLESS=1 to suppress the video window
# emulator window for interactive use. # (CI / batch runs that only care about crash.txt).
video_arg="-video none" video_arg="-window"
if [[ "${MAME_WINDOW:-0}" = "1" ]]; then if [[ "${MAME_HEADLESS:-0}" = "1" ]]; then
video_arg="-window" video_arg="-video none"
fi fi
# Clear the done-marker the Lua hook uses to signal shutdown. # Clear the done-marker the Lua hook uses to signal shutdown.
@ -182,13 +284,14 @@ mame apple2gs \
-flop3 "$work/boot.po" \ -flop3 "$work/boot.po" \
-flop4 "$work/joey.2mg" \ -flop4 "$work/joey.2mg" \
$video_arg -sound none \ $video_arg -sound none \
-nothrottle \
-debug -debuglog \ -debug -debuglog \
-autoboot_script "$work/crash-hook.lua" & -autoboot_script "$work/crash-hook.lua" &
mame_pid=$! mame_pid=$!
# Poll for the done-marker. Kill MAME once Lua signals it. Cap total # Poll for the done-marker. Kill MAME once Lua signals it. Cap total
# wall-clock at 60 s in case MAME never writes the marker. # wall-clock at 6 min so we don't outlive the Lua-side watchdog (5 min).
deadline=$((SECONDS + 60)) deadline=$((SECONDS + 360))
while kill -0 "$mame_pid" 2>/dev/null; do while kill -0 "$mame_pid" 2>/dev/null; do
if [[ -f $out/.done ]]; then if [[ -f $out/.done ]]; then
kill "$mame_pid" 2>/dev/null kill "$mame_pid" 2>/dev/null

View file

@ -11,13 +11,18 @@
# #
# scripts/run-iigs.sh # boots (Pattern hint) # scripts/run-iigs.sh # boots (Pattern hint)
# scripts/run-iigs.sh hello # boots, hints HELLO # scripts/run-iigs.sh hello # boots, hints HELLO
# scripts/run-iigs.sh keys # boots, hints KEYS # scripts/run-iigs.sh draw # boots, hints DRAW
# scripts/run-iigs.sh joy # boots, hints JOY #
# scripts/run-iigs.sh sprite # boots, hints SPRITE # Argument is any built example name (case-insensitive); upper-case
# scripts/run-iigs.sh audio # boots, hints AUDIO # it for the Finder hint and existence-check.
set -euo pipefail set -euo pipefail
if [[ $# -gt 1 ]]; then
echo "usage: $0 [example-name]" >&2
exit 2
fi
prog=${1:-pattern} prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
@ -33,10 +38,17 @@ sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg data_disk=$repo/build/iigs/bin/joey.2mg
null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom
case $prog in target=${prog^^}
hello|pattern|keys|joy|sprite|audio) ;; bin_dir=$repo/build/iigs/bin
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;; if [[ ! -f "$bin_dir/$target" ]]; then
esac echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
if compgen -G "$bin_dir/*" > /dev/null; then
echo "available examples in $bin_dir:" >&2
find "$bin_dir" -maxdepth 1 -type f -printf '%f\n' \
| grep -vE '\.2mg$|\.txt$' >&2 || true
fi
exit 1
fi
for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do
if [[ ! -f $f ]]; then if [[ ! -f $f ]]; then
@ -107,7 +119,6 @@ cp "$data_disk" "$work/joey.2mg"
# install_support_iigs_null_c600. # install_support_iigs_null_c600.
cp "$null_c600" "$work/c600.rom" cp "$null_c600" "$work/c600.rom"
target=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
cat <<EOF cat <<EOF
GSplus launching GS/OS 6.0.4. GSplus launching GS/OS 6.0.4.
Once Finder is up: Once Finder is up:

View file

@ -8,13 +8,31 @@
#include <string.h> #include <string.h>
#include "joey/draw.h" #include "joey/draw.h"
#include "joey/debug.h"
#include "hal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// On IIgs, hoist all primitive functions out of _ROOT into a named
// DRAWPRIMS load segment. drawLine/drawCircle/fillCircle/floodFill/
// floodFillBounded together push past the 64 KB-per-bank budget for
// the simpler binaries (PATTERN was the first to fail). On other
// ports this macro vanishes.
JOEYLIB_SEGMENT("DRAWPRIMS")
// ----- Constants -----
// Flood-fill seed stack: each entry is (x, y) = 4 bytes, so 512 slots
// = 2 KB. For 320x200 surfaces with reasonable region sizes this is
// well above the worst-case scanline-fill seed depth (typically <50).
// On overflow the fill silently truncates rather than crashing.
#define FLOOD_STACK_SIZE 512
// ----- Prototypes ----- // ----- Prototypes -----
static bool blitClip(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h, int16_t srcW, int16_t srcH); static bool blitClip(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h, int16_t srcW, int16_t srcH);
static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible); static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible);
static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void floodFillInternal(SurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
static uint8_t srcPixel(const uint8_t *row, int16_t x); static uint8_t srcPixel(const uint8_t *row, int16_t x);
static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble); static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble);
@ -117,6 +135,207 @@ static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_
} }
// Smith's scanline flood fill. Implements both the unbounded and the
// boundary-stopped variants in one pass: the matching predicate is
// (pixel == matchColor) when matchEqual is true (unbounded floodFill,
// matchColor is the original seed color) or (pixel != matchColor)
// when matchEqual is false (floodFillBounded, matchColor is the
// boundary that stops the fill).
//
// Algorithm:
// 1. Push seed (x, y) on stack.
// 2. Pop a seed; skip if its pixel no longer matches (already
// filled by an earlier span overlap).
// 3. Scan left and right from the seed to find the longest run of
// matching pixels containing it -- this is the current span.
// 4. Fill the span with newColor.
// 5. Walk the row above and the row below, scanning the columns
// that overlap the just-filled span; for each contiguous run of
// matching pixels, push the rightmost x of that run as a new
// seed (so popping that seed next will scan the same run).
// 6. Repeat until the stack drains.
//
// Stack overflow truncates the fill rather than crashing; for vector
// art (Sierra-style picture playback) the input is well-behaved and
// 512 entries is plenty.
static void floodFillInternal(SurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual) {
static int16_t stackX[FLOOD_STACK_SIZE];
static int16_t stackY[FLOOD_STACK_SIZE];
static uint8_t floodMarkBuf[SURFACE_WIDTH];
int16_t sp;
int16_t x;
int16_t y;
int16_t leftX;
int16_t rightX;
uint8_t *row;
uint8_t pix;
bool pixMatch;
uint8_t newNibble;
newNibble = (uint8_t)(newColor & 0x0F);
matchColor = (uint8_t)(matchColor & 0x0F);
sp = 0;
stackX[sp] = startX;
stackY[sp] = startY;
sp++;
while (sp > 0) {
sp--;
x = stackX[sp];
y = stackY[sp];
if (y < 0 || y >= SURFACE_HEIGHT || x < 0 || x >= SURFACE_WIDTH) {
continue;
}
row = &s->pixels[y * SURFACE_BYTES_PER_ROW];
// Highest-tier asm fast path: seed-test + walk-left + walk-right
// + 1-row fill + scan-above + scan-below + push, all in one
// cross-segment call. The asm caches row addr / match decoder
// across every sub-operation. C just pops and dispatches; this
// path completes the entire per-seed work.
{
bool seedMatched;
if (halFastFloodWalkAndScans(s->pixels, x, y,
matchColor, newNibble, matchEqual,
stackX, stackY,
&sp, FLOOD_STACK_SIZE,
&seedMatched, &leftX, &rightX)) {
continue;
}
}
// Tier-2 asm fast path: combined seed test + walk-left +
// walk-right in one cross-segment call. Falls back to the
// pure-C walks below on ports without an asm implementation.
{
bool seedMatched;
if (halFastFloodWalk(row, x, matchColor, newNibble, matchEqual,
&seedMatched, &leftX, &rightX)) {
if (!seedMatched) {
continue;
}
} else {
pix = srcPixel(row, x);
pixMatch = (pix == matchColor);
if (matchEqual) {
if (!pixMatch) {
continue;
}
} else {
if (pixMatch || pix == newNibble) {
continue;
}
}
// Walk left to find the start of the matching run.
leftX = x;
while (leftX > 0) {
pix = srcPixel(row, (int16_t)(leftX - 1));
pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break;
}
leftX--;
}
// Walk right to find the end.
rightX = x;
while (rightX < SURFACE_WIDTH - 1) {
pix = srcPixel(row, (int16_t)(rightX + 1));
pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break;
}
rightX++;
}
}
}
// Fill the span. Bypass fillRect's clipping wrapper: walk-out
// already guaranteed leftX/rightX are in [0..SURFACE_WIDTH-1]
// and the seed-pop bounds check did the same for y.
{
int16_t spanW = (int16_t)(rightX - leftX + 1);
if (!halFastFillRect(s, leftX, y, (uint16_t)spanW, 1, newNibble)) {
fillRectClipped(s, leftX, y, spanW, 1, newNibble);
}
}
// Scan rows above and below for run boundaries. The hot
// per-pixel match check goes through halFastFloodScanRow on
// ports that have it (IIgs); fills markBuf[] with 1/0 per
// pixel so the run-edge walk below is array-only -- no
// function call, no nibble extract.
{
int16_t i;
int16_t spanLen;
uint8_t *scanRow;
int16_t scanY;
int16_t side;
bool curHit;
bool prevHit;
spanLen = (int16_t)(rightX - leftX + 1);
for (side = 0; side < 2; side++) {
if (side == 0) {
if (y <= 0) {
continue;
}
scanY = (int16_t)(y - 1);
} else {
if (y >= SURFACE_HEIGHT - 1) {
continue;
}
scanY = (int16_t)(y + 1);
}
scanRow = &s->pixels[scanY * SURFACE_BYTES_PER_ROW];
// Prefer the combined scan+push asm path (one call per
// scan, no markBuf and no per-pixel C edge walk).
if (!halFastFloodScanAndPush(scanRow, leftX, rightX,
matchColor, newNibble, matchEqual,
scanY, stackX, stackY,
&sp, FLOOD_STACK_SIZE)) {
if (!halFastFloodScanRow(scanRow, leftX, rightX,
matchColor, newNibble, matchEqual,
floodMarkBuf)) {
// C fallback: fill markBuf the slow way.
for (i = 0; i < spanLen; i++) {
pix = srcPixel(scanRow, (int16_t)(leftX + i));
pixMatch = (pix == matchColor);
floodMarkBuf[i] = (uint8_t)(matchEqual
? (pixMatch ? 1 : 0)
: ((!pixMatch && pix != newNibble) ? 1 : 0));
}
}
// Walk markBuf for run-edge transitions.
prevHit = false;
for (i = 0; i < spanLen; i++) {
curHit = floodMarkBuf[i] != 0;
if (!curHit && prevHit) {
if (sp < FLOOD_STACK_SIZE) {
stackX[sp] = (int16_t)(leftX + i - 1);
stackY[sp] = scanY;
sp++;
}
}
prevHit = curHit;
}
if (prevHit) {
if (sp < FLOOD_STACK_SIZE) {
stackX[sp] = rightX;
stackY[sp] = scanY;
sp++;
}
}
}
}
}
}
}
static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble) { static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble) {
uint8_t *byte; uint8_t *byte;
@ -142,6 +361,131 @@ static uint8_t srcPixel(const uint8_t *row, int16_t x) {
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
void drawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
int16_t x;
int16_t y;
int16_t err;
int16_t ir;
if (s == NULL) {
return;
}
if (r == 0) {
drawPixel(s, cx, cy, colorIndex);
return;
}
// Fast path: when the bounding circle is fully on-surface we can
// hand off to the port asm (no per-pixel bounds check needed in
// the inner loop) and mark the bounding box dirty once.
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
halFastDrawCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
return;
}
// Bresenham midpoint: maintain (x, y) on the perimeter, eight-
// octant symmetry plots all 8 reflections each iteration. Routes
// through drawPixel so off-surface pixels clip individually.
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
drawPixel(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
drawPixel(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
drawPixel(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
drawPixel(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
drawPixel(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
drawPixel(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
drawPixel(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
drawPixel(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
y++;
if (err <= 0) {
err = (int16_t)(err + 2 * y + 1);
} else {
x--;
err = (int16_t)(err + 2 * (y - x) + 1);
}
}
}
void drawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
int16_t dx;
int16_t dy;
int16_t sx;
int16_t sy;
int16_t err;
int16_t e2;
int16_t tmp;
if (s == NULL) {
return;
}
// Horizontal and vertical fast paths use fillRect; the general
// case Bresenham routes per-pixel through drawPixel so per-pixel
// off-surface clipping just works.
if (y0 == y1) {
if (x0 > x1) {
tmp = x0;
x0 = x1;
x1 = tmp;
}
fillRect(s, x0, y0, (uint16_t)(x1 - x0 + 1), 1, colorIndex);
return;
}
if (x0 == x1) {
if (y0 > y1) {
tmp = y0;
y0 = y1;
y1 = tmp;
}
fillRect(s, x0, y0, 1, (uint16_t)(y1 - y0 + 1), colorIndex);
return;
}
// Diagonal: if both endpoints are on-surface, the inner Bresenham
// can run without per-pixel bound checks. Hand off to the port
// fast path; bounding-box dirty marking happens here in C either
// way.
if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH &&
y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT &&
halFastDrawLine(s, x0, y0, x1, y1, colorIndex)) {
int16_t bbx = (x0 < x1) ? x0 : x1;
int16_t bby = (y0 < y1) ? y0 : y1;
int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1);
int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1);
surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh);
return;
}
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
sx = (int16_t)((x0 < x1) ? 1 : -1);
sy = (int16_t)((y0 < y1) ? 1 : -1);
err = (int16_t)(dx + dy);
while (1) {
drawPixel(s, x0, y0, colorIndex);
if (x0 == x1 && y0 == y1) {
break;
}
e2 = (int16_t)(2 * err);
if (e2 >= dy) {
err = (int16_t)(err + dy);
x0 = (int16_t)(x0 + sx);
}
if (e2 <= dx) {
err = (int16_t)(err + dx);
y0 = (int16_t)(y0 + sy);
}
}
}
void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
uint8_t *byte; uint8_t *byte;
uint8_t nibble; uint8_t nibble;
@ -153,6 +497,7 @@ void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
return; return;
} }
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex)) {
byte = &s->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)]; byte = &s->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
nibble = colorIndex & 0x0F; nibble = colorIndex & 0x0F;
if (x & 1) { if (x & 1) {
@ -160,6 +505,83 @@ void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
} else { } else {
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
} }
}
surfaceMarkDirtyRect(s, x, y, 1, 1);
}
void drawRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
if (s == NULL) {
return;
}
if (w == 0 || h == 0) {
return;
}
// Degenerate dimensions: a 1xN or Nx1 rect IS a line, and a 1x1
// rect is a single pixel. fillRect handles both correctly so we
// don't need to fork the inner logic.
if (h == 1 || w == 1) {
fillRect(s, x, y, w, h, colorIndex);
return;
}
// Top edge.
fillRect(s, x, y, w, 1, colorIndex);
// Bottom edge.
fillRect(s, x, (int16_t)(y + (int16_t)h - 1), w, 1, colorIndex);
// Left edge (interior only -- top and bottom corners already drawn).
fillRect(s, x, (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex);
// Right edge (interior only).
fillRect(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex);
}
void fillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
int16_t y;
int16_t x;
int16_t ir;
uint16_t xx;
uint16_t yy;
uint16_t r2;
if (s == NULL) {
return;
}
if (r == 0) {
drawPixel(s, cx, cy, colorIndex);
return;
}
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
halFastFillCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
return;
}
// For each y from 0 to r, find the largest x such that x*x + y*y
// <= r*r and emit a horizontal span. Maintain xx=x*x, yy=y*y
// incrementally so the hot loop never does a 32-bit multiply --
// critical on 65816 / 68000 / 286 where mul is slow or absent.
// (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. r is uint16_t
// so xx, yy, r2 fit in uint16_t for any r where x*x+y*y can equal
// r2 (i.e. r <= 255 -> r2 <= 65025).
xx = (uint16_t)(r * r);
r2 = xx;
yy = 0;
x = (int16_t)r;
for (y = 0; y <= (int16_t)r; y++) {
while (xx + yy > r2) {
xx = (uint16_t)(xx - (uint16_t)(2 * x - 1));
x--;
}
fillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), (uint16_t)(2 * x + 1), 1, colorIndex);
if (y > 0) {
fillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), (uint16_t)(2 * x + 1), 1, colorIndex);
}
yy = (uint16_t)(yy + (uint16_t)(2 * y + 1));
}
} }
@ -182,7 +604,53 @@ void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t
if (!visible) { if (!visible) {
return; return;
} }
if (!halFastFillRect(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex)) {
fillRectClipped(s, sx, sy, sw, sh, colorIndex); fillRectClipped(s, sx, sy, sw, sh, colorIndex);
}
surfaceMarkDirtyRect(s, sx, sy, sw, sh);
}
void floodFill(SurfaceT *s, int16_t x, int16_t y, uint8_t newColor) {
uint8_t *row;
uint8_t seedColor;
if (s == NULL) {
return;
}
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return;
}
row = &s->pixels[y * SURFACE_BYTES_PER_ROW];
seedColor = srcPixel(row, x);
if ((seedColor & 0x0F) == (newColor & 0x0F)) {
return;
}
floodFillInternal(s, x, y, newColor, seedColor, true);
}
void floodFillBounded(SurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor) {
uint8_t *row;
uint8_t pix;
if (s == NULL) {
return;
}
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return;
}
row = &s->pixels[y * SURFACE_BYTES_PER_ROW];
pix = srcPixel(row, x);
// Starting on a boundary pixel or already-filled pixel: nothing
// to do.
if ((pix & 0x0F) == (boundaryColor & 0x0F)) {
return;
}
if ((pix & 0x0F) == (newColor & 0x0F)) {
return;
}
floodFillInternal(s, x, y, newColor, boundaryColor, false);
} }
@ -225,6 +693,10 @@ void surfaceBlit(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y) {
} }
srcRowBytes = (int16_t)((src->width + 1) >> 1); srcRowBytes = (int16_t)((src->width + 1) >> 1);
srcRow = &src->pixels[srcY0 * srcRowBytes];
dstRow = &dst->pixels[y * SURFACE_BYTES_PER_ROW];
if (!halFastBlitRect(dstRow, x, srcRow, srcX0,
copyW, copyH, srcRowBytes, 0xFFFFu)) {
for (row = 0; row < copyH; row++) { for (row = 0; row < copyH; row++) {
srcRow = &src->pixels[(srcY0 + row) * srcRowBytes]; srcRow = &src->pixels[(srcY0 + row) * srcRowBytes];
dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW]; dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW];
@ -233,6 +705,8 @@ void surfaceBlit(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y) {
dstPixel(dstRow, x + col, nibble); dstPixel(dstRow, x + col, nibble);
} }
} }
}
surfaceMarkDirtyRect(dst, x, y, copyW, copyH);
} }
@ -259,6 +733,10 @@ void surfaceBlitMasked(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t
transparent = (uint8_t)(transparentIndex & 0x0F); transparent = (uint8_t)(transparentIndex & 0x0F);
srcRowBytes = (int16_t)((src->width + 1) >> 1); srcRowBytes = (int16_t)((src->width + 1) >> 1);
srcRow = &src->pixels[srcY0 * srcRowBytes];
dstRow = &dst->pixels[y * SURFACE_BYTES_PER_ROW];
if (!halFastBlitRect(dstRow, x, srcRow, srcX0,
copyW, copyH, srcRowBytes, (uint16_t)transparent)) {
for (row = 0; row < copyH; row++) { for (row = 0; row < copyH; row++) {
srcRow = &src->pixels[(srcY0 + row) * srcRowBytes]; srcRow = &src->pixels[(srcY0 + row) * srcRowBytes];
dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW]; dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW];
@ -270,6 +748,8 @@ void surfaceBlitMasked(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t
dstPixel(dstRow, x + col, nibble); dstPixel(dstRow, x + col, nibble);
} }
} }
}
surfaceMarkDirtyRect(dst, x, y, copyW, copyH);
} }
@ -282,5 +762,8 @@ void surfaceClear(SurfaceT *s, uint8_t colorIndex) {
} }
nibble = colorIndex & 0x0F; nibble = colorIndex & 0x0F;
doubled = (uint8_t)((nibble << 4) | nibble); doubled = (uint8_t)((nibble << 4) | nibble);
if (!halFastSurfaceClear(s, doubled)) {
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
}
surfaceMarkDirtyAll(s);
} }

View file

@ -23,6 +23,14 @@ bool halInit(const JoeyConfigT *config);
// Per-port teardown. Restores display mode, frees HW-adjacent buffers. // Per-port teardown. Restores display mode, frees HW-adjacent buffers.
void halShutdown(void); void halShutdown(void);
// Allocate / release the SURFACE_PIXELS_SIZE-byte pixel buffer that
// backs the library-owned stage surface. Ports that have a
// hardware-friendly pin location for the back buffer (IIgs $01/2000
// with SHR shadow inhibited) return that address here; ports with no
// such constraint just malloc/free.
uint8_t *halStageAllocPixels(void);
void halStageFreePixels(uint8_t *pixels);
// Present the entire source surface to the display. // Present the entire source surface to the display.
void halPresent(const SurfaceT *src); void halPresent(const SurfaceT *src);
@ -64,4 +72,116 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
void halAudioStopSfx(uint8_t slot); void halAudioStopSfx(uint8_t slot);
void halAudioFrameTick(void); void halAudioFrameTick(void);
// Optional fast-path hooks. Each returns true if the port handled the
// operation in a port-specific accelerated path; false means the
// caller should fall back to the platform-agnostic C implementation.
//
// Funneling all asm dispatches through hal.c (one TU per port) avoids
// the cumulative ORCA Linker "Expression too complex" failure that
// hits when multiple cross-platform TUs each call into a named load
// segment full of asm primitives. Cross-platform code in src/core/
// only ever calls into HAL, so the link-time expression cost is paid
// once per binary -- not once per TU that wants speed.
//
// Each port must provide all of these; ports without an accelerated
// path simply return false from every hook.
bool halFastSurfaceClear(SurfaceT *s, uint8_t doubled);
bool halFastFillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex);
bool halFastTileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord);
// Tile primitives operate on already-computed row-0 pointers from
// the C wrapper. dstRow0 / srcRow0 point at the first byte of the
// 8x8 region within their respective surfaces (stride 160). For
// tilePaste / tileSnap the TileT side is a packed 32-byte buffer
// (stride 4); the corresponding pointer points at byte 0 of that
// buffer.
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0);
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent);
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels);
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0);
// drawPixel inner: caller has already done NULL + bounds checks.
// (x, y) are guaranteed in [0..SURFACE_WIDTH-1] x [0..SURFACE_HEIGHT-1].
// colorIndex is the 0..15 nibble. Surface dirty marking happens in
// the C wrapper after this returns.
bool halFastDrawPixel(SurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex);
// drawLine inner for the diagonal case. Caller ensures both endpoints
// are inside the surface bounds, so the inner loop runs without
// per-pixel clip checks. The C wrapper still routes pure horizontal
// and vertical lines through fillRect (which has its own fast path).
bool halFastDrawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex);
// drawCircle / fillCircle inner. Caller has already validated that
// the entire bounding circle (cx-r .. cx+r, cy-r .. cy+r) fits inside
// the surface bounds, so the inner loop plots every octant pixel
// unconditionally. r is guaranteed > 0; the cx == 0 / r == 0 cases
// stay in the C wrapper.
bool halFastDrawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
bool halFastFillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
// floodFill helper: combined seed test + walk-left + walk-right for
// one row. Returns true if the port handled it (asm path taken). The
// out-param seedMatched tells the caller whether the seed pixel
// satisfied the match criterion -- if false, caller skips this pop;
// if true, leftXOut/rightXOut hold the run boundaries.
// Returns false if no asm path; caller falls back to C walks.
bool halFastFloodWalk(uint8_t *row, int16_t startX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
bool *seedMatched,
int16_t *leftXOut, int16_t *rightXOut);
// floodFill helper for the row-above / row-below run-detection scans.
// Walks pixels [leftX..rightX] inclusive of `row`, writing 1 byte per
// pixel into markBuf (1 = qualifies for flood, 0 = does not). The C
// side then walks markBuf for run-edge transitions, replacing the
// per-pixel srcPixel + match check inside the inner loop.
// Returns true if the port handled it; false to fall back to C.
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
uint8_t *markBuf);
// Combined per-pixel scan + run-edge walk + seed push. Higher-level
// than halFastFloodScanRow: replaces both the markBuf fill AND the C
// loop that walks markBuf for falling edges. *spInOut is read on entry
// and updated with the new top-of-stack on return. Returns true if
// the port handled it (caller skips the C run-edge walk entirely);
// false to fall back to halFastFloodScanRow + C walk.
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
int16_t scanY,
int16_t *stackX, int16_t *stackY,
int16_t *spInOut, int16_t maxSp);
// Highest-level flood helper: combined seed-test + walk-left + walk-right
// + scan-above + scan-below + push for ONE popped seed. Replaces three
// cross-segment HAL calls (halFastFloodWalk + 2x halFastFloodScanAndPush)
// per dispatch loop iteration with one. The asm internally caches row
// addr / matchByte / nibble decoder across all three sub-operations.
//
// pixels is the surface base (s->pixels). On return, leftXOut / rightXOut
// hold the matching-run boundaries (only valid if seedMatched != 0); the
// caller does the 1-row halFastFillRect using those bounds. *spInOut is
// updated with any new seeds the asm pushed for the row above/below.
//
// Returns true if the port handled it; false to fall back to
// halFastFloodWalk + the per-side halFastFloodScanAndPush calls.
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
int16_t *stackX, int16_t *stackY,
int16_t *spInOut, int16_t maxSp,
bool *seedMatched,
int16_t *leftXOut, int16_t *rightXOut);
// surfaceBlit / surfaceBlitMasked rect-copy helper. Caller has done
// the clip math: dstRow0 / srcRow0 point at row 0 of the source/dest
// regions, dstX / srcX are intra-row pixel offsets, copyW/copyH are
// the clipped extents. dst stride is hardcoded SURFACE_BYTES_PER_ROW.
// transparent == $FFFF means opaque (always copy); any 0..15 value
// means src nibbles equal to that index are skipped.
// Returns true if the port handled it; false to fall back to C.
bool halFastBlitRect(uint8_t *dstRow0, int16_t dstX,
const uint8_t *srcRow0, int16_t srcX,
int16_t copyW, int16_t copyH, int16_t srcRowBytes,
uint16_t transparent);
#endif #endif

View file

@ -57,23 +57,26 @@ bool joeyInit(const JoeyConfigT *config) {
memcpy(&gConfig, config, sizeof(gConfig)); memcpy(&gConfig, config, sizeof(gConfig));
if (!surfaceAllocScreen()) { // halInit must run before stageAlloc: on IIgs the stage's pixel
setError("failed to allocate screen surface"); // buffer comes from halStageAllocPixels, which depends on shadow /
// SHR setup that halInit performs.
if (!halInit(&gConfig)) {
const char *halMsg = halLastError();
setError(halMsg != NULL ? halMsg : "halInit failed");
return false;
}
if (!stageAlloc()) {
setError("failed to allocate stage surface");
halShutdown();
return false; return false;
} }
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes
: DEFAULT_CODEGEN_BYTES)) { : DEFAULT_CODEGEN_BYTES)) {
setError("failed to allocate codegen arena"); setError("failed to allocate codegen arena");
surfaceFreeScreen(); stageFree();
return false; halShutdown();
}
if (!halInit(&gConfig)) {
const char *halMsg = halLastError();
setError(halMsg != NULL ? halMsg : "halInit failed");
codegenArenaShutdown();
surfaceFreeScreen();
return false; return false;
} }
@ -99,9 +102,9 @@ void joeyShutdown(void) {
return; return;
} }
halInputShutdown(); halInputShutdown();
halShutdown();
codegenArenaShutdown(); codegenArenaShutdown();
surfaceFreeScreen(); stageFree();
halShutdown();
gInitialized = false; gInitialized = false;
clearError(); clearError();
} }

View file

@ -38,6 +38,25 @@ void joeyInputPoll(void) {
} }
void joeyWaitForAnyKey(void) {
int16_t i;
// Prime the previous-state snapshot so a key already held when the
// wait starts has to be released and re-pressed (rising edge) to
// satisfy the wait. Otherwise auto-repeat or a key still down from
// the previous frame would exit instantly.
joeyInputPoll();
while (1) {
joeyInputPoll();
for (i = (int16_t)(KEY_NONE + 1); i < (int16_t)KEY_COUNT; i++) {
if (joeyKeyPressed((JoeyKeyE)i)) {
return;
}
}
}
}
bool joeyKeyDown(JoeyKeyE key) { bool joeyKeyDown(JoeyKeyE key) {
if (key <= KEY_NONE || key >= KEY_COUNT) { if (key <= KEY_NONE || key >= KEY_COUNT) {
return false; return false;

View file

@ -1,32 +1,40 @@
// Present / slam dispatcher. // Stage present dispatcher.
// //
// Validates and clips the source rectangle, then routes to the port's // stagePresent walks the per-row dirty bands set by drawing primitives
// HAL implementation for the actual pixel format conversion and // and asks the port HAL to flip just those rows to the display, then
// display-memory write. // resets the dirty state. stagePresentRect bypasses dirty tracking
// entirely and slams a caller-specified rectangle (after clipping).
#include <stddef.h> #include <stddef.h>
#include "joey/debug.h"
#include "joey/present.h" #include "joey/present.h"
#include "hal.h" #include "hal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
void surfacePresent(const SurfaceT *src) { void stagePresent(void) {
if (src == NULL) { SurfaceT *stage;
stage = stageGet();
if (stage == NULL) {
return; return;
} }
halPresent(src); halPresent(stage);
stageDirtyClearAll();
} }
void surfacePresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { void stagePresentRect(int16_t x, int16_t y, uint16_t w, uint16_t h) {
SurfaceT *stage;
int16_t sx; int16_t sx;
int16_t sy; int16_t sy;
int16_t sw; int16_t sw;
int16_t sh; int16_t sh;
if (src == NULL) { stage = stageGet();
if (stage == NULL) {
return; return;
} }
@ -59,5 +67,5 @@ void surfacePresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, u
return; return;
} }
halPresentRect(src, sx, sy, (uint16_t)sw, (uint16_t)sh); halPresentRect(stage, sx, sy, (uint16_t)sw, (uint16_t)sh);
} }

View file

@ -153,6 +153,7 @@ static void spriteDrawInterpreted(SurfaceT *s, SpriteT *sp, int16_t x, int16_t y
writeDstNibble(dstRow, (int16_t)(dx + col), nibble); writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
} }
} }
surfaceMarkDirtyRect(s, dx, dy, w, h);
} }
@ -276,6 +277,7 @@ void spriteDraw(SurfaceT *s, SpriteT *sp, int16_t x, int16_t y) {
// need clip math (they walk fixed offsets). // need clip math (they walk fixed offsets).
if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) { if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
spriteCompiledDraw(s, sp, x, y); spriteCompiledDraw(s, sp, x, y);
surfaceMarkDirtyRect(s, x, y, (int16_t)widthPx, (int16_t)heightPx);
return; return;
} }
spriteDrawInterpreted(s, sp, x, y); spriteDrawInterpreted(s, sp, x, y);
@ -556,6 +558,8 @@ void spriteRestoreUnder(SurfaceT *s, const SpriteBackupT *backup) {
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1; shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
if (sp->routineOffsets[shift][SPRITE_OP_RESTORE] != SPRITE_NOT_COMPILED) { if (sp->routineOffsets[shift][SPRITE_OP_RESTORE] != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup); spriteCompiledRestoreUnder(s, backup);
surfaceMarkDirtyRect(s, backup->x, backup->y,
(int16_t)backup->width, (int16_t)backup->height);
return; return;
} }
} }
@ -568,6 +572,8 @@ void spriteRestoreUnder(SurfaceT *s, const SpriteBackupT *backup) {
&backup->bytes[(uint16_t)row * (uint16_t)copyBytes], &backup->bytes[(uint16_t)row * (uint16_t)copyBytes],
(size_t)copyBytes); (size_t)copyBytes);
} }
surfaceMarkDirtyRect(s, backup->x, backup->y,
(int16_t)backup->width, (int16_t)backup->height);
} }

View file

@ -1,5 +1,5 @@
// Surface allocation, destruction, persistence, and the library-owned // Surface allocation, destruction, persistence, and the library-owned
// screen surface. // stage (the back-buffer surface).
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
@ -7,6 +7,7 @@
#include <string.h> #include <string.h>
#include "joey/surface.h" #include "joey/surface.h"
#include "hal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t)) #define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t))
@ -19,20 +20,52 @@
// ----- Module state ----- // ----- Module state -----
static SurfaceT *gScreen = NULL; static SurfaceT *gStage = NULL;
uint8_t gStageMinWord[SURFACE_HEIGHT];
uint8_t gStageMaxWord[SURFACE_HEIGHT];
// ----- Internal helpers (alphabetical) -----
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
if (minWord < gStageMinWord[y]) {
gStageMinWord[y] = minWord;
}
if (maxWord > gStageMaxWord[y]) {
gStageMaxWord[y] = maxWord;
}
}
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
SurfaceT *stageGet(void) {
return gStage;
}
void surfaceCopy(SurfaceT *dst, const SurfaceT *src) { void surfaceCopy(SurfaceT *dst, const SurfaceT *src) {
if (dst == NULL || src == NULL || dst == src) { if (dst == NULL || src == NULL || dst == src) {
return; return;
} }
memcpy(dst, src, sizeof(SurfaceT)); memcpy(dst->pixels, src->pixels, SURFACE_PIXELS_SIZE);
memcpy(dst->scb, src->scb, sizeof(src->scb));
memcpy(dst->palette, src->palette, sizeof(src->palette));
surfaceMarkDirtyAll(dst);
} }
SurfaceT *surfaceCreate(void) { SurfaceT *surfaceCreate(void) {
SurfaceT *s = (SurfaceT *)calloc(1, sizeof(SurfaceT)); SurfaceT *s;
s = (SurfaceT *)calloc(1, sizeof(SurfaceT));
if (s == NULL) {
return NULL;
}
s->pixels = (uint8_t *)calloc(1, SURFACE_PIXELS_SIZE);
if (s->pixels == NULL) {
free(s);
return NULL;
}
return s; return s;
} }
@ -41,18 +74,14 @@ void surfaceDestroy(SurfaceT *s) {
if (s == NULL) { if (s == NULL) {
return; return;
} }
if (s == gScreen) { if (s == gStage) {
return; return;
} }
free(s->pixels);
free(s); free(s);
} }
SurfaceT *surfaceGetScreen(void) {
return gScreen;
}
bool surfaceLoadFile(SurfaceT *dst, const char *path) { bool surfaceLoadFile(SurfaceT *dst, const char *path) {
FILE *fp; FILE *fp;
long fileSize; long fileSize;
@ -90,6 +119,7 @@ bool surfaceLoadFile(SurfaceT *dst, const char *path) {
return false; return false;
} }
fclose(fp); fclose(fp);
surfaceMarkDirtyAll(dst);
return true; return true;
} }
@ -121,21 +151,81 @@ bool surfaceSaveFile(const SurfaceT *src, const char *path) {
} }
// ----- Internal (alphabetical) ----- void surfaceMarkDirtyAll(const SurfaceT *s) {
int16_t row;
bool surfaceAllocScreen(void) { if (s != gStage) {
if (gScreen != NULL) {
return true;
}
gScreen = (SurfaceT *)calloc(1, sizeof(SurfaceT));
return gScreen != NULL;
}
void surfaceFreeScreen(void) {
if (gScreen == NULL) {
return; return;
} }
free(gScreen); for (row = 0; row < SURFACE_HEIGHT; row++) {
gScreen = NULL; gStageMinWord[row] = 0;
gStageMaxWord[row] = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
}
}
// Drawing primitives pass the rect they actually wrote (already
// clipped to surface bounds, w and h positive). For non-stage surfaces
// the call is a no-op so primitives can call unconditionally without
// branching themselves.
void surfaceMarkDirtyRect(const SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h) {
int16_t row;
int16_t yEnd;
uint8_t minWord;
uint8_t maxWord;
if (s != gStage) {
return;
}
if (w <= 0 || h <= 0) {
return;
}
minWord = (uint8_t)(x >> 2);
maxWord = (uint8_t)((x + w - 1) >> 2);
yEnd = y + h;
for (row = y; row < yEnd; row++) {
widenRow(row, minWord, maxWord);
}
}
// ----- Internal (alphabetical) -----
bool stageAlloc(void) {
if (gStage != NULL) {
return true;
}
gStage = (SurfaceT *)calloc(1, sizeof(SurfaceT));
if (gStage == NULL) {
return false;
}
gStage->pixels = halStageAllocPixels();
if (gStage->pixels == NULL) {
free(gStage);
gStage = NULL;
return false;
}
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
stageDirtyClearAll();
return true;
}
void stageDirtyClearAll(void) {
int16_t row;
for (row = 0; row < SURFACE_HEIGHT; row++) {
gStageMinWord[row] = STAGE_DIRTY_CLEAN_MIN;
gStageMaxWord[row] = STAGE_DIRTY_CLEAN_MAX;
}
}
void stageFree(void) {
if (gStage == NULL) {
return;
}
halStageFreePixels(gStage->pixels);
free(gStage);
gStage = NULL;
} }

View file

@ -7,15 +7,57 @@
#include "joey/surface.h" #include "joey/surface.h"
// Pixels are reached through a pointer rather than an inline array so
// that the per-port HAL can pin the stage's pixel buffer to a specific
// hardware-friendly address (e.g. IIgs $01/2000 with SHR shadow
// inhibited at $C035 so writes stay in fast bank $01 instead of
// auto-mirroring to $E1). Caller-side `s->pixels[i]` syntax is
// unchanged; only allocation/copy paths in surface.c shift to a
// two-buffer model.
struct SurfaceT { struct SurfaceT {
uint8_t pixels[SURFACE_PIXELS_SIZE]; uint8_t *pixels;
uint8_t scb[SURFACE_HEIGHT]; uint8_t scb[SURFACE_HEIGHT];
uint16_t palette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; uint16_t palette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
}; };
// Allocate and free the library's pre-allocated screen surface. Called // 16-bit words per scanline. SHR / chunky 4bpp packed = 2 px per byte,
// from init.c during joeyInit / joeyShutdown. // 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words.
bool surfaceAllocScreen(void); // Dirty tracking grain is 16-bit words because that matches the IIgs
void surfaceFreeScreen(void); // PEI / PHA slam unit and the Amiga / ST c2p group is 16 px = 4 words.
#define SURFACE_WORDS_PER_ROW (SURFACE_BYTES_PER_ROW / 2)
// Sentinels for "row is clean": min > max can never happen for a real
// dirty range, so the present loop tests `min > max` to skip a row.
#define STAGE_DIRTY_CLEAN_MIN 0xFFu
#define STAGE_DIRTY_CLEAN_MAX 0x00u
// Per-row dirty word bands for the stage. gStageMinWord[y] is the
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
// is the rightmost (inclusive). Both default to the CLEAN sentinels
// after stageAlloc and after each stagePresent.
extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
// Drawing primitives call this with their already-clipped destination
// rect. If `s` is the stage, the affected rows' [minWord, maxWord]
// bands are widened to cover the rect. If `s` is any other surface,
// the call is a no-op -- non-stage surfaces never get presented, so
// they don't carry dirty state.
void surfaceMarkDirtyRect(const SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h);
// Shorthand for "every row, full width" -- used by surfaceClear and
// the bulk-replace paths (surfaceCopy, surfaceLoadFile). No-op if `s`
// is not the stage.
void surfaceMarkDirtyAll(const SurfaceT *s);
// Reset every row to CLEAN. Called by stagePresent after the slam.
void stageDirtyClearAll(void);
// Allocate and free the library-owned stage (the back-buffer surface
// that stagePresent flips to the display). Called from init.c during
// joeyInit / joeyShutdown. The stage's pixel storage is supplied by
// the port HAL via halStageAllocPixels.
bool stageAlloc(void);
void stageFree(void);
#endif #endif

280
src/core/tile.c Normal file
View file

@ -0,0 +1,280 @@
// Tiles as 8x8 surface regions. The whole API is byte-shoveling
// between SurfaceT regions (or between a SurfaceT region and a
// stack TileT buffer); no separate tileset container, no allocator.
//
// Block coords (bx, by) map to pixel (bx*8, by*8). At 4bpp packed
// each tile row is 4 bytes wide, so byte-aligned memcpy is the inner
// loop for everything except the masked / transparent variant, which
// has to read-modify-write each byte to preserve destination pixels
// under transparent (color-0 by convention) source nibbles.
#include <stddef.h>
#include "joey/tile.h"
#include "hal.h"
#include "surfaceInternal.h"
// (No <string.h> -- the 4-byte-per-row inner copies are spelled out
// inline below. Avoiding memcpy / memset from the DRAWPRIMS load
// segment keeps cross-bank relocation references out of 13/SysLib;
// without that the ORCA Linker hits "Expression too complex" on
// the small-binary builds.)
// Hoist tile primitives into the DRAWPRIMS load segment. Asm
// dispatches go through halFast* hooks in src/port/iigs/hal.c so
// only one TU references the asm symbols (avoids the cumulative
// "Expression too complex" link failure).
JOEYLIB_SEGMENT("DRAWPRIMS")
// ----- Prototypes -----
static void copyTileOpaque(uint8_t *dst, const uint8_t *src);
static void copyTileMasked(uint8_t *dst, const uint8_t *src, uint8_t transparent);
// ----- Internal helpers (alphabetical) -----
// 32-byte block copy with the surface row stride between rows. dst
// and src already point at the row-0 first byte of their respective
// 8x8 regions.
static void copyTileOpaque(uint8_t *dst, const uint8_t *src) {
uint8_t row;
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
dst += SURFACE_BYTES_PER_ROW;
src += SURFACE_BYTES_PER_ROW;
}
}
// Same as copyTileOpaque but treats source nibbles equal to
// `transparent` as skip-through. The src bytes are inspected nibble-
// by-nibble; only non-transparent nibbles overwrite the destination.
static void copyTileMasked(uint8_t *dst, const uint8_t *src, uint8_t transparent) {
uint8_t row;
uint8_t col;
uint8_t srcByte;
uint8_t dstByte;
uint8_t transHi;
uint8_t transLo;
uint8_t srcHi;
uint8_t srcLo;
transHi = (uint8_t)((transparent & 0x0F) << 4);
transLo = (uint8_t)(transparent & 0x0F);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
for (col = 0; col < TILE_BYTES_PER_ROW; col++) {
srcByte = src[col];
srcHi = (uint8_t)(srcByte & 0xF0);
srcLo = (uint8_t)(srcByte & 0x0F);
// Both nibbles transparent: skip the byte entirely.
if (srcHi == transHi && srcLo == transLo) {
continue;
}
dstByte = dst[col];
if (srcHi != transHi) {
dstByte = (uint8_t)((dstByte & 0x0F) | srcHi);
}
if (srcLo != transLo) {
dstByte = (uint8_t)((dstByte & 0xF0) | srcLo);
}
dst[col] = dstByte;
}
dst += SURFACE_BYTES_PER_ROW;
src += SURFACE_BYTES_PER_ROW;
}
}
// ----- Public API (alphabetical) -----
void drawText(SurfaceT *dst, uint8_t bx, uint8_t by, const SurfaceT *fontSurface, const uint16_t *asciiMap, const char *str) {
uint16_t entry;
uint8_t cx;
uint8_t cy;
uint8_t ch;
uint8_t srcBx;
uint8_t srcBy;
if (dst == NULL || fontSurface == NULL || asciiMap == NULL || str == NULL) {
return;
}
cx = bx;
cy = by;
while (*str != '\0') {
ch = (uint8_t)*str++;
entry = asciiMap[ch];
if (entry != TILE_NO_GLYPH) {
srcBx = (uint8_t)(entry & 0x00FFu);
srcBy = (uint8_t)((entry >> 8) & 0x00FFu);
tileCopyMasked(dst, cx, cy, fontSurface, srcBx, srcBy, 0u);
}
cx++;
if (cx >= TILE_BLOCKS_PER_ROW) {
cx = 0;
cy++;
if (cy >= TILE_BLOCKS_PER_COL) {
return;
}
}
}
}
void tileCopy(SurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const SurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint8_t *dstRow0;
const uint8_t *srcRow0;
uint16_t dstPixelX;
uint16_t dstPixelY;
uint16_t srcPixelX;
uint16_t srcPixelY;
if (dst == NULL || src == NULL) {
return;
}
if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE);
srcPixelY = (uint16_t)((uint16_t)srcBy * TILE_PIXELS_PER_SIDE);
dstRow0 = &dst->pixels[dstPixelY * SURFACE_BYTES_PER_ROW + (dstPixelX >> 1)];
srcRow0 = &src->pixels[srcPixelY * SURFACE_BYTES_PER_ROW + (srcPixelX >> 1)];
if (!halFastTileCopy(dstRow0, srcRow0)) {
copyTileOpaque(dstRow0, srcRow0);
}
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void tileCopyMasked(SurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const SurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
uint8_t *dstRow0;
const uint8_t *srcRow0;
uint16_t dstPixelX;
uint16_t dstPixelY;
uint16_t srcPixelX;
uint16_t srcPixelY;
if (dst == NULL || src == NULL) {
return;
}
if (dstBx >= TILE_BLOCKS_PER_ROW || dstBy >= TILE_BLOCKS_PER_COL ||
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE);
srcPixelY = (uint16_t)((uint16_t)srcBy * TILE_PIXELS_PER_SIDE);
dstRow0 = &dst->pixels[dstPixelY * SURFACE_BYTES_PER_ROW + (dstPixelX >> 1)];
srcRow0 = &src->pixels[srcPixelY * SURFACE_BYTES_PER_ROW + (srcPixelX >> 1)];
if (!halFastTileCopyMasked(dstRow0, srcRow0, transparentIndex)) {
copyTileMasked(dstRow0, srcRow0, transparentIndex);
}
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void tileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint8_t doubled;
uint16_t pixelX;
uint16_t pixelY;
if (s == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
doubled = (uint8_t)(((colorIndex & 0x0F) << 4) | (colorIndex & 0x0F));
if (!halFastTileFill(s, bx, by,
(uint16_t)((uint16_t)doubled | ((uint16_t)doubled << 8)))) {
uint8_t *row = &s->pixels[pixelY * SURFACE_BYTES_PER_ROW + (pixelX >> 1)];
uint8_t i;
for (i = 0; i < TILE_PIXELS_PER_SIDE; i++) {
row[0] = doubled;
row[1] = doubled;
row[2] = doubled;
row[3] = doubled;
row += SURFACE_BYTES_PER_ROW;
}
}
surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void tilePaste(SurfaceT *dst, uint8_t bx, uint8_t by, const TileT *in) {
uint8_t *dstRow;
const uint8_t *src;
uint16_t pixelX;
uint16_t pixelY;
uint8_t row;
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
dstRow = &dst->pixels[pixelY * SURFACE_BYTES_PER_ROW + (pixelX >> 1)];
src = &in->pixels[0];
if (!halFastTilePaste(dstRow, src)) {
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dstRow[0] = src[0];
dstRow[1] = src[1];
dstRow[2] = src[2];
dstRow[3] = src[3];
dstRow += SURFACE_BYTES_PER_ROW;
src += TILE_BYTES_PER_ROW;
}
}
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void tileSnap(const SurfaceT *src, uint8_t bx, uint8_t by, TileT *out) {
const uint8_t *srcRow;
uint8_t *dst;
uint16_t pixelX;
uint16_t pixelY;
uint8_t row;
if (src == NULL || out == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
srcRow = &src->pixels[pixelY * SURFACE_BYTES_PER_ROW + (pixelX >> 1)];
dst = &out->pixels[0];
if (!halFastTileSnap(dst, srcRow)) {
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dst[0] = srcRow[0];
dst[1] = srcRow[1];
dst[2] = srcRow[2];
dst[3] = srcRow[3];
srcRow += SURFACE_BYTES_PER_ROW;
dst += TILE_BYTES_PER_ROW;
}
}
}

View file

@ -20,6 +20,7 @@
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <exec/types.h> #include <exec/types.h>
@ -439,7 +440,7 @@ bool halInit(const JoeyConfigT *config) {
} }
// Force COLOR00 to black so the overscan/border region around the // Force COLOR00 to black so the overscan/border region around the
// 320x200 display is black until the app's palette load takes over // 320x200 display is black until the app's palette load takes over
// on the first surfacePresent. Apps that paint a non-black bg need // on the first stagePresent. Apps that paint a non-black bg need
// do nothing -- their palette[0] writes the same COLOR00 once the // do nothing -- their palette[0] writes the same COLOR00 once the
// first LoadRGB4 fires from uploadScbAndPalette. // first LoadRGB4 fires from uploadScbAndPalette.
SetRGB4(&gScreen->ViewPort, 0, 0, 0, 0); SetRGB4(&gScreen->ViewPort, 0, 0, 0, 0);
@ -453,11 +454,30 @@ const char *halLastError(void) {
void halPresent(const SurfaceT *src) { void halPresent(const SurfaceT *src) {
int16_t y;
uint8_t minWord;
uint8_t maxWord;
uint16_t byteStart;
uint16_t byteEnd;
if (src == NULL || gScreen == NULL) { if (src == NULL || gScreen == NULL) {
return; return;
} }
updateCopperIfNeeded(src); updateCopperIfNeeded(src);
c2pRange(src, 0, SURFACE_HEIGHT, 0, AMIGA_BYTES_PER_ROW);
// Walk per-row dirty bands: each planar byte covers 8 px = 2 chunky
// words, so byteStart = minWord/2 and byteEnd = maxWord/2 + 1
// converts dirty-word units to the planar-byte units c2pRange wants.
for (y = 0; y < SURFACE_HEIGHT; y++) {
minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
if (minWord > maxWord) {
continue;
}
byteStart = (uint16_t)(minWord >> 1);
byteEnd = (uint16_t)((maxWord >> 1) + 1);
c2pRange(src, y, (int16_t)(y + 1), byteStart, byteEnd);
}
} }
@ -507,3 +527,183 @@ void halShutdown(void) {
gNewUCL = NULL; gNewUCL = NULL;
} }
} }
// Amiga has no asm fast paths yet; cross-platform code falls back to
// its C implementations whenever these return false.
bool halFastSurfaceClear(SurfaceT *s, uint8_t doubled) {
(void)s;
(void)doubled;
return false;
}
bool halFastFillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
return false;
}
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0;
(void)srcRow0;
return false;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0;
(void)srcRow0;
(void)transparent;
return false;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0;
(void)srcTilePixels;
return false;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels;
(void)srcRow0;
return false;
}
bool halFastDrawPixel(SurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)colorIndex;
return false;
}
bool halFastDrawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
(void)s;
(void)x0;
(void)y0;
(void)x1;
(void)y1;
(void)colorIndex;
return false;
}
bool halFastDrawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastBlitRect(uint8_t *dstRow0, int16_t dstX, const uint8_t *srcRow0, int16_t srcX, int16_t copyW, int16_t copyH, int16_t srcRowBytes, uint16_t transparent) {
(void)dstRow0;
(void)dstX;
(void)srcRow0;
(void)srcX;
(void)copyW;
(void)copyH;
(void)srcRowBytes;
(void)transparent;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastTileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)s;
(void)bx;
(void)by;
(void)fillWord;
return false;
}
uint8_t *halStageAllocPixels(void) {
return (uint8_t *)malloc(SURFACE_PIXELS_SIZE);
}
void halStageFreePixels(uint8_t *pixels) {
free(pixels);
}

View file

@ -29,6 +29,7 @@
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <mint/osbind.h> #include <mint/osbind.h>
@ -497,11 +498,30 @@ const char *halLastError(void) {
void halPresent(const SurfaceT *src) { void halPresent(const SurfaceT *src) {
int16_t y;
uint8_t minWord;
uint8_t maxWord;
uint16_t groupStart;
uint16_t groupEnd;
if (src == NULL || !gModeSet) { if (src == NULL || !gModeSet) {
return; return;
} }
refreshPaletteStateIfNeeded(src); refreshPaletteStateIfNeeded(src);
c2pRange(src, 0, SURFACE_HEIGHT, 0, ST_GROUPS_PER_ROW);
// Walk per-row dirty bands: each c2p group covers 16 px = 4 chunky
// words, so groupStart = minWord/4 and groupEnd = maxWord/4 + 1
// converts dirty-word units to c2pRange's group units.
for (y = 0; y < SURFACE_HEIGHT; y++) {
minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
if (minWord > maxWord) {
continue;
}
groupStart = (uint16_t)(minWord >> 2);
groupEnd = (uint16_t)((maxWord >> 2) + 1);
c2pRange(src, y, (int16_t)(y + 1), groupStart, groupEnd);
}
} }
@ -563,3 +583,183 @@ void halShutdown(void) {
writeDiagnostics(); writeDiagnostics();
gModeSet = false; gModeSet = false;
} }
// ST has no asm fast paths yet; cross-platform code falls back to its
// C implementations when these return false.
bool halFastSurfaceClear(SurfaceT *s, uint8_t doubled) {
(void)s;
(void)doubled;
return false;
}
bool halFastFillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
return false;
}
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0;
(void)srcRow0;
return false;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0;
(void)srcRow0;
(void)transparent;
return false;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0;
(void)srcTilePixels;
return false;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels;
(void)srcRow0;
return false;
}
bool halFastDrawPixel(SurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)colorIndex;
return false;
}
bool halFastDrawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
(void)s;
(void)x0;
(void)y0;
(void)x1;
(void)y1;
(void)colorIndex;
return false;
}
bool halFastDrawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastBlitRect(uint8_t *dstRow0, int16_t dstX, const uint8_t *srcRow0, int16_t srcX, int16_t copyW, int16_t copyH, int16_t srcRowBytes, uint16_t transparent) {
(void)dstRow0;
(void)dstX;
(void)srcRow0;
(void)srcX;
(void)copyW;
(void)copyH;
(void)srcRowBytes;
(void)transparent;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastTileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)s;
(void)bx;
(void)by;
(void)fillWord;
return false;
}
uint8_t *halStageAllocPixels(void) {
return (uint8_t *)malloc(SURFACE_PIXELS_SIZE);
}
void halStageFreePixels(uint8_t *pixels) {
free(pixels);
}

View file

@ -13,6 +13,7 @@
#include <signal.h> #include <signal.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <dpmi.h> #include <dpmi.h>
@ -217,13 +218,28 @@ const char *halLastError(void) {
void halPresent(const SurfaceT *src) { void halPresent(const SurfaceT *src) {
int16_t y; int16_t y;
uint8_t minWord;
uint8_t maxWord;
int16_t pixelX;
uint16_t pixelW;
if (src == NULL || gVgaMem == NULL) { if (src == NULL || gVgaMem == NULL) {
return; return;
} }
uploadPaletteIfNeeded(src); uploadPaletteIfNeeded(src);
// Walk per-row dirty bands: each chunky word holds 4 mode-13h
// bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
// gives the byte range expandAndWriteLine needs.
for (y = 0; y < SURFACE_HEIGHT; y++) { for (y = 0; y < SURFACE_HEIGHT; y++) {
expandAndWriteLine(src, y, 0, SURFACE_WIDTH, &gVgaMem[y * VGA_STRIDE]); minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
if (minWord > maxWord) {
continue;
}
pixelX = (int16_t)((uint16_t)minWord << 2);
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
} }
} }
@ -277,3 +293,183 @@ void halShutdown(void) {
gCrashLog = NULL; gCrashLog = NULL;
} }
} }
// DOS has no asm fast paths yet; cross-platform code falls back to
// its C implementations when these return false.
bool halFastSurfaceClear(SurfaceT *s, uint8_t doubled) {
(void)s;
(void)doubled;
return false;
}
bool halFastFillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
return false;
}
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0;
(void)srcRow0;
return false;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0;
(void)srcRow0;
(void)transparent;
return false;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0;
(void)srcTilePixels;
return false;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels;
(void)srcRow0;
return false;
}
bool halFastDrawPixel(SurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)colorIndex;
return false;
}
bool halFastDrawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
(void)s;
(void)x0;
(void)y0;
(void)x1;
(void)y1;
(void)colorIndex;
return false;
}
bool halFastDrawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastBlitRect(uint8_t *dstRow0, int16_t dstX, const uint8_t *srcRow0, int16_t srcX, int16_t copyW, int16_t copyH, int16_t srcRowBytes, uint16_t transparent) {
(void)dstRow0;
(void)dstX;
(void)srcRow0;
(void)srcX;
(void)copyW;
(void)copyH;
(void)srcRowBytes;
(void)transparent;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastTileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)s;
(void)bx;
(void)by;
(void)fillWord;
return false;
}
uint8_t *halStageAllocPixels(void) {
return (uint8_t *)malloc(SURFACE_PIXELS_SIZE);
}
void halStageFreePixels(uint8_t *pixels) {
free(pixels);
}

View file

@ -1,70 +0,0 @@
// Apple IIgs audio HAL stub. Real implementation pending.
//
// Pipeline already in place:
// * toolchains/install.sh fetches Ninjaforce NTP source into
// toolchains/iigs/ntp/ (CRLF stripped on extraction).
// * make/iigs.mk assembles ninjatrackerplus.s with Merlin32 into
// build/iigs/audio/ntpplayer.bin (34 KB raw 65816, originally
// org $0F0000 but bank-internal so it relocates at any bank-start
// load address).
// * package-disk.sh bundles ntpplayer.bin onto the IIgs disk image
// alongside the demo binaries.
//
// Why this file is still a stub:
// The runtime load path (NewHandle + fopen + fread on NTPPLAYER.BIN)
// pulls Memory Manager + ORCA stdio into the link, and ORCA Linker
// fails with "Expression too complex in 13/SysLib" when those are
// added on top of the existing graphics + input HAL plumbing for
// *every* demo (the IIgs build links each binary as one monolithic
// image, no static-library culling). Bringing in those tool sets
// needs to land alongside the JSL trampoline that actually uses the
// loaded NTP -- one combined effort, with the demos that don't need
// audio either kept on a thinner audio shim or split out so the
// linker isn't asked to resolve everything for everyone.
//
// Until that combined load + trampoline iteration ships, every entry
// here is a safe no-op so the audio API stays callable.
#include "hal.h"
bool halAudioInit(void) {
return false;
}
void halAudioShutdown(void) {
}
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
(void)data;
(void)length;
(void)loop;
}
void halAudioStopMod(void) {
}
bool halAudioIsPlayingMod(void) {
return false;
}
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
(void)slot;
(void)sample;
(void)length;
(void)rateHz;
}
void halAudioStopSfx(uint8_t slot) {
(void)slot;
}
void halAudioFrameTick(void) {
}

View file

@ -1,7 +1,13 @@
// Apple IIgs audio HAL -- full version (linked only into the AUDIO // Apple IIgs audio HAL -- single source linked into every IIgs demo.
// demo via make/iigs.mk's split source set). audio.c keeps the no-op // Earlier we split this into an audio.c no-op stub and an audio_full.c
// stub for every other demo so the monolithic IIgs link budget stays // real implementation, filtering audio_full.c out of non-AUDIO source
// safe. // sets, because pulling Memory Manager + the 34 KB NTP replayer into
// every binary blew the ORCA Linker's blank-segment / "Expression too
// complex" budget. Now that we know how to name load segments (see
// ORCA/C ch. 30 "segment statement"), we put every function in this
// file into a named AUDIOIMPL load segment; the GS/OS loader places
// it in its own bank, so non-AUDIO binaries pay only for the data
// references, not the implementation code.
// //
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time // The NinjaTrackerPlus replayer is Merlin32-assembled at build time
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the // to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
@ -17,7 +23,25 @@
#include "hal.h" #include "hal.h"
#include "joey/audio.h" #include "joey/audio.h"
#include "ntpplayer_data.h"
// Place every function defined below in the shared DRAWPRIMS overflow
// load segment so the linker keeps the implementation code out of
// _ROOT in every binary that includes this TU. (See ORCA/C ch. 30
// "segment statement". Reusing the same segment as draw.c / tile.c
// rather than picking a unique name keeps the linker's symbol-
// resolution expressions flat -- per-name extras nest the
// expression and trip the "too complex" threshold on small
// binaries.)
//
// The 34 KB NTP replayer bytes are NOT in this segment -- ORCA/C's
// `segment` statement only relocates functions, not data. They live
// in their own NTPDATA load segment, declared in build/iigs/audio/
// ntpdata.asm (auto-generated from ntpplayer.bin by make/iigs.mk).
// We just extern the symbols here.
segment "DRAWPRIMS";
extern const unsigned char gNtpPlayerBytes[];
extern const unsigned long gNtpPlayerBytes_len;
// ----- Constants ----- // ----- Constants -----

View file

@ -10,26 +10,118 @@
// ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so // ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so
// that the long addresses resolve to bank $E1. // that the long addresses resolve to bank $E1.
// //
// For M1 this is a simple direct-copy present. PEI-slam (in assembly) // DIRTY-WALK + PEI-SLAM PRESENT
// arrives as an optimization in a later milestone; the structure here // -----------------------------
// is unchanged -- only halPresent / halPresentRect get faster inner // halPresent walks the per-row dirty bands maintained by drawing
// loops. // primitives in src/core/*.c. Fully-dirty rows go through the PEI
// slam in src/port/iigs/peislam.asm (~530 cyc/row, ~55% faster than
// memcpy/MVN); partial-dirty rows use memcpy, which ORCA-C lowers
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into
// bank $E1 when the dirty band is too narrow to amortize the slam's
// per-call AUXWRITE/RAMRD/shadow toggle.
//
// peislam.asm declares its load segment as DRAWPRIMS so the linker
// places it in its own bank, separate from AUDIO's _ROOT (where
// audio_full.c + Memory Manager + stdio + NTPstreamsound already
// crowd up against the 64 KB-per-bank limit).
#include <stddef.h> #include <stddef.h>
#include <string.h> #include <string.h>
#include "joey/debug.h"
#include "hal.h" #include "hal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// hal.c is the single TU that calls into joeyDraw.asm. Cross-
// platform draw.c / tile.c / etc. dispatch through halFast*
// functions defined here; they never reference the asm symbols
// directly. This avoids the cumulative ORCA-Linker-Expression-
// too-complex-in-13/SysLib failure that hit when each cross-
// platform TU brought its own asm extern.
JOEYLIB_SEGMENT("DRAWPRIMS")
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
// PEI-slam fill of `bytesPerRow` doubled bytes per row across `rows`
// rows, advancing 160 bytes per row. firstRow must be in bank $01.
// Caller handles partial-nibble edges in C; bytesPerRow is even.
extern void iigsFillRectStageInner(uint8_t *firstRow, uint16_t bytesPerRow, uint16_t rows, uint16_t fillWord);
// 16 STA abs,X stores at fixed offsets along a 160-byte stride.
// ~120 cyc per call.
extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t fillWord);
// Tile copy / paste / snap inner loops. All take 4-byte large-
// model pointers; bank may differ between dst and src (heap
// surface vs stage). Stride contracts:
// tileCopyInner / tileCopyMaskedInner: dst 160, src 160
// tilePasteInner: dst 160, src 4
// tileSnapInner: dst 4, src 160
extern void iigsTileCopyInner(uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
extern void iigsTilePasteInner(uint8_t *dstRow0, const uint8_t *srcTilePixels);
extern void iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0);
// Single-pixel and Bresenham line plot. drawLine inner takes
// pre-clipped endpoints (caller validates against surface bounds);
// it does no per-pixel clipping in the loop.
extern void iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble);
extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble);
// Bresenham midpoint circle outline. Caller has verified the entire
// bbox is on-surface so no per-pixel clip.
extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
// Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette.
// Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette.
// Replaces ORCA-C's memcpy path which silently fails when called
// from halPresent (DBR-state quirk after prior asm primitives).
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr);
// floodFill row walk: tests seed pixel and walks left/right to find
// the matching run. Writes results to gFloodSeedMatch / gFloodLeftX /
// gFloodRightX (DRAWPRIMS globals).
extern void iigsFloodWalkInner(uint8_t *row, uint16_t startX, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual);
extern uint16_t gFloodSeedMatch;
extern uint16_t gFloodLeftX;
extern uint16_t gFloodRightX;
// Per-pixel match scan over [leftX..rightX] of `row`. Writes 1/0 to
// markBuf[i] for each pixel. matchEqual selects boundary vs equal mode
// (see C srcPixel match logic).
extern void iigsFloodScanRowInner(uint8_t *row, uint16_t leftX, uint16_t rightX, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, uint8_t *markBuf);
// Per-pixel rect blit (src->dst). transparent == $FFFF means opaque
// (always copy); else pixels with src nibble == (transparent & $0F)
// are skipped. Dst stride is hardcoded 160 (SURFACE_BYTES_PER_ROW).
extern void iigsBlitRectInner(uint8_t *dstRow0, uint16_t dstX, const uint8_t *srcRow0, uint16_t srcX, uint16_t copyW, uint16_t copyH, uint16_t srcRowBytes, uint16_t transparent);
// Combined scan + push: matches each pixel, tracks run state, pushes
// (x, scanY) to the (stackX, stackY) arrays at *spInOut on every
// falling edge and at the end of the row if still in a run. *spInOut
// is read on entry and updated with the new top-of-stack on return.
extern void iigsFloodScanAndPushInner(uint8_t *row, uint16_t leftX, uint16_t rightX, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, uint16_t scanY, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp);
// Single-call per-popped-seed worker: seed test + walk-left + walk-right
// + scan-above + scan-below + push, all sharing cached row addr and
// match decoders. Outputs to gFloodSeedMatch / gFloodLeftX / gFloodRightX.
extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp);
// One-shot init for the y*160 lookup table (gRowOffsetLut, 400 bytes
// in DRAWPRIMS data). Called once from halInit. After this returns,
// every asm primitive that needs row offset can do `lda >lut,x` instead
// of the 7-instruction shift-add.
extern void iigsInitRowLut(void);
// Filled circle, scanline-style. fillWord low byte is the doubled
// nibble (e.g., 0x33 for nibble 3).
extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
// ----- Hardware addresses (24-bit / long pointers) ----- // ----- Hardware addresses (24-bit / long pointers) -----
#define IIGS_NEWVIDEO_REG ((volatile uint8_t *)0x00C029L) #define IIGS_NEWVIDEO_REG ((volatile uint8_t *)0x00C029L)
#define IIGS_BORDER_REG ((volatile uint8_t *)0x00C034L) #define IIGS_BORDER_REG ((volatile uint8_t *)0x00C034L)
#define IIGS_SHADOW_REG ((volatile uint8_t *)0x00C035L)
#define IIGS_VBL_STATUS ((volatile uint8_t *)0x00C019L) #define IIGS_VBL_STATUS ((volatile uint8_t *)0x00C019L)
#define IIGS_SHR_PIXELS ((uint8_t *)0xE12000L) #define IIGS_SHR_PIXELS ((uint8_t *)0xE12000L)
#define IIGS_SHR_SCB ((uint8_t *)0xE19D00L) #define IIGS_SHR_SCB ((uint8_t *)0xE19D00L)
#define IIGS_SHR_PALETTE ((uint16_t *)0xE19E00L) #define IIGS_SHR_PALETTE ((uint16_t *)0xE19E00L)
// The stage lives at $01/2000 -- the same offset as the SHR display
// framebuffer at $E1/2000, but in the fast (2.8 MHz) bank. With SHR
// shadow inhibited at $C035, writes here are NOT auto-mirrored to
// $E1, so drawing is full-speed and isolated from the displayed
// frame until the next stagePresent.
#define IIGS_STAGE_PIXELS ((uint8_t *)0x012000L)
#define VBL_BAR_BIT 0x80 #define VBL_BAR_BIT 0x80
// NEWVIDEO bit masks // NEWVIDEO bit masks
@ -41,6 +133,15 @@
// handler) and bumps its "Code: RED" status. Always include this bit. // handler) and bumps its "Code: RED" status. Always include this bit.
#define NEWVIDEO_RESERVED_BIT 0x01 #define NEWVIDEO_RESERVED_BIT 0x01
// $C035 SHADOW register: bit set = shadow INHIBITED for that range.
// Bit 1 = hi-res page 1 ($02000-$03FFF in bank $01)
// Bit 2 = hi-res page 2 ($04000-$05FFF in bank $01)
// Bit 3 = SHR ($02000-$09FFF in bank $01)
// We set 1+2+3 because the SHR pixel range overlaps both hi-res
// pages; leaving any of those shadows live would silently mirror
// part of the stage to $E1.
#define SHADOW_INHIBIT_SHR_MASK 0x0E
// $C034 BORDER register: high nibble = beep/IRQ enables (preserve), // $C034 BORDER register: high nibble = beep/IRQ enables (preserve),
// low nibble = border color index 0..15. Color 0 is the all-zero // low nibble = border color index 0..15. Color 0 is the all-zero
// palette entry by IIgs convention; we force the low nibble to 0 // palette entry by IIgs convention; we force the low nibble to 0
@ -51,6 +152,7 @@
static uint8_t gPreviousNewVideo = 0; static uint8_t gPreviousNewVideo = 0;
static uint8_t gPreviousBorder = 0; static uint8_t gPreviousBorder = 0;
static uint8_t gPreviousShadow = 0;
static bool gModeSet = false; static bool gModeSet = false;
// Last-uploaded SCB and palette. Both registers live in bank $E1; on a // Last-uploaded SCB and palette. Both registers live in bank $E1; on a
@ -62,6 +164,22 @@ static uint8_t gCachedScb [SURFACE_HEIGHT];
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false; static bool gCacheValid = false;
// PEI slam scratch shared with src/port/iigs/peislam.asm. File-scope
// non-static so the asm can `ext` them; all accesses inside the slam
// use long-mode addressing so they bypass the //e RAMRD redirect the
// slam turns on for the duration of the run.
volatile uint16_t gPeiOrigSp;
volatile uint8_t gPeiOrigShadow;
volatile uint16_t gPeiTempRowBase;
// Defined in src/port/iigs/peislam.asm, in its own load segment
// (DRAWPRIMS) so the GS/OS loader places it in a different bank from
// AUDIO's _ROOT. PEI-slams the full 80 words of stage row `y` into
// the matching $E1 SHR row, ~530 cyc/row vs ~1120 cyc for memcpy/MVN.
extern void peiSlamFullRow(int16_t y);
// Upload SCB and palette into bank-$E1 SHR memory only when they have // Upload SCB and palette into bank-$E1 SHR memory only when they have
// changed since the last call. paletteOrScbChanged returns false when // changed since the last call. paletteOrScbChanged returns false when
// the cache is already in sync, in which case both memcpys to $E1 are // the cache is already in sync, in which case both memcpys to $E1 are
@ -86,8 +204,18 @@ bool halInit(const JoeyConfigT *config) {
(void)config; (void)config;
gPreviousNewVideo = *IIGS_NEWVIDEO_REG; gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
gPreviousBorder = *IIGS_BORDER_REG; gPreviousBorder = *IIGS_BORDER_REG;
gPreviousShadow = *IIGS_SHADOW_REG;
*IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE | NEWVIDEO_RESERVED_BIT); *IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE | NEWVIDEO_RESERVED_BIT);
*IIGS_BORDER_REG = (uint8_t)(gPreviousBorder & BORDER_COLOR_MASK); *IIGS_BORDER_REG = (uint8_t)(gPreviousBorder & BORDER_COLOR_MASK);
// Inhibit shadowing of the stage region. Without this, every
// write to $01/2000-9FFF mirrors to $E1 and the off-screen-buffer
// illusion breaks (the user would see drawing in progress).
*IIGS_SHADOW_REG = (uint8_t)(gPreviousShadow | SHADOW_INHIBIT_SHR_MASK);
// SCB and palette are uploaded by halPresent's iigsBlitStageToShr
// (asm path, MVN to bank $E1). C-side memset/memcpy to bank $E1
// is unreliable from halInit's calling context, so we don't try
// it here -- the first present will set up SCB to 320 mode.
iigsInitRowLut();
gModeSet = true; gModeSet = true;
return true; return true;
} }
@ -102,8 +230,13 @@ void halPresent(const SurfaceT *src) {
if (src == NULL) { if (src == NULL) {
return; return;
} }
uploadScbAndPaletteIfNeeded(src); // iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
memcpy(IIGS_SHR_PIXELS, src->pixels, SURFACE_PIXELS_SIZE); // upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
// ORCA-C's C-side memcpy to bank $E1 has been unreliable from
// halPresent's calling context, so we route everything through
// the asm path. Future: re-introduce per-row dirty-band logic
// for partial-screen updates (currently we always blit 32K).
iigsBlitStageToShr(src->scb, &src->palette[0][0]);
} }
@ -134,6 +267,270 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1
} }
void halShutdown(void) {
if (gModeSet) {
*IIGS_NEWVIDEO_REG = gPreviousNewVideo;
*IIGS_BORDER_REG = gPreviousBorder;
*IIGS_SHADOW_REG = gPreviousShadow;
gModeSet = false;
}
}
bool halFastSurfaceClear(SurfaceT *s, uint8_t doubled) {
uint16_t fillWord;
if (s == NULL) {
return false;
}
if (s != stageGet()) {
return false;
}
fillWord = (uint16_t)((uint16_t)doubled | ((uint16_t)doubled << 8));
iigsSurfaceClearInner(s->pixels, fillWord);
return true;
}
bool halFastFillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
int16_t pxStart;
int16_t pxEnd;
int16_t midStart;
int16_t midBytes;
int16_t trailingByte;
int16_t leadingByte;
bool hasLeading;
bool hasTrailing;
int16_t row;
uint8_t *line;
uint16_t fillWord;
uint8_t nibble;
uint8_t doubled;
if (s == NULL) {
return false;
}
if (s != stageGet()) {
return false;
}
pxStart = x;
pxEnd = (int16_t)(x + (int16_t)w);
leadingByte = (int16_t)(pxStart >> 1);
hasLeading = (pxStart & 1) != 0;
if (hasLeading) {
pxStart++;
}
midStart = (int16_t)(pxStart >> 1);
midBytes = (int16_t)((pxEnd - pxStart) >> 1);
hasTrailing = ((pxEnd - pxStart) & 1) != 0;
trailingByte = (int16_t)(midStart + midBytes);
if (midBytes <= 0) {
return false;
}
nibble = (uint8_t)(colorIndex & 0x0F);
doubled = (uint8_t)((nibble << 4) | nibble);
if (hasLeading || hasTrailing) {
for (row = 0; row < (int16_t)h; row++) {
line = &s->pixels[(y + row) * SURFACE_BYTES_PER_ROW];
if (hasLeading) {
line[leadingByte] = (uint8_t)((line[leadingByte] & 0xF0) | nibble);
}
if (hasTrailing) {
line[trailingByte] = (uint8_t)((line[trailingByte] & 0x0F) | (nibble << 4));
}
}
}
fillWord = (uint16_t)((uint16_t)doubled | ((uint16_t)doubled << 8));
line = &s->pixels[y * SURFACE_BYTES_PER_ROW + midStart];
iigsFillRectStageInner(line, (uint16_t)midBytes, h, fillWord);
return true;
}
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
iigsTileCopyInner(dstRow0, srcRow0);
return true;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
iigsTileCopyMaskedInner(dstRow0, srcRow0, (uint16_t)transparent);
return true;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
iigsTilePasteInner(dstRow0, srcTilePixels);
return true;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
iigsTileSnapInner(dstTilePixels, srcRow0);
return true;
}
bool halFastDrawPixel(SurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
if (s == NULL) {
return false;
}
iigsDrawPixelInner(s->pixels, x, y, (uint16_t)(colorIndex & 0x0F));
return true;
}
bool halFastDrawLine(SurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
if (s == NULL) {
return false;
}
iigsDrawLineInner(s->pixels,
(uint16_t)x0, (uint16_t)y0,
(uint16_t)x1, (uint16_t)y1,
(uint16_t)(colorIndex & 0x0F));
return true;
}
bool halFastDrawCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
if (s == NULL) {
return false;
}
iigsDrawCircleInner(s->pixels,
(uint16_t)cx, (uint16_t)cy, r,
(uint16_t)(colorIndex & 0x0F));
return true;
}
bool halFastFillCircle(SurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
uint16_t fillWord;
uint8_t nibble;
uint8_t doubled;
if (s == NULL) {
return false;
}
if (s != stageGet()) {
return false;
}
nibble = (uint8_t)(colorIndex & 0x0F);
doubled = (uint8_t)((nibble << 4) | nibble);
fillWord = (uint16_t)((uint16_t)doubled | ((uint16_t)doubled << 8));
iigsFillCircleInner(s->pixels, (uint16_t)cx, (uint16_t)cy, r, fillWord);
return true;
}
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
if (row == NULL || seedMatched == NULL || leftXOut == NULL || rightXOut == NULL) {
return false;
}
iigsFloodWalkInner(row, (uint16_t)startX,
(uint16_t)(matchColor & 0x0F),
(uint16_t)(newColor & 0x0F),
(uint16_t)(matchEqual ? 1 : 0));
*seedMatched = (gFloodSeedMatch != 0);
if (*seedMatched) {
*leftXOut = (int16_t)gFloodLeftX;
*rightXOut = (int16_t)gFloodRightX;
}
return true;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
if (row == NULL || markBuf == NULL) {
return false;
}
iigsFloodScanRowInner(row, (uint16_t)leftX, (uint16_t)rightX,
(uint16_t)(matchColor & 0x0F),
(uint16_t)(newColor & 0x0F),
(uint16_t)(matchEqual ? 1 : 0),
markBuf);
return true;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
if (row == NULL || stackX == NULL || stackY == NULL || spInOut == NULL) {
return false;
}
iigsFloodScanAndPushInner(row,
(uint16_t)leftX, (uint16_t)rightX,
(uint16_t)(matchColor & 0x0F),
(uint16_t)(newColor & 0x0F),
(uint16_t)(matchEqual ? 1 : 0),
(uint16_t)scanY,
stackX, stackY,
(uint16_t *)spInOut,
(uint16_t)maxSp);
return true;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
if (pixels == NULL || stackX == NULL || stackY == NULL || spInOut == NULL || seedMatched == NULL || leftXOut == NULL || rightXOut == NULL) {
return false;
}
iigsFloodWalkAndScansInner(pixels,
(uint16_t)x, (uint16_t)y,
(uint16_t)(matchColor & 0x0F),
(uint16_t)(newColor & 0x0F),
(uint16_t)(matchEqual ? 1 : 0),
stackX, stackY,
(uint16_t *)spInOut,
(uint16_t)maxSp);
*seedMatched = (gFloodSeedMatch != 0);
*leftXOut = (int16_t)gFloodLeftX;
*rightXOut = (int16_t)gFloodRightX;
return true;
}
bool halFastBlitRect(uint8_t *dstRow0, int16_t dstX, const uint8_t *srcRow0, int16_t srcX, int16_t copyW, int16_t copyH, int16_t srcRowBytes, uint16_t transparent) {
if (dstRow0 == NULL || srcRow0 == NULL || copyW <= 0 || copyH <= 0) {
return false;
}
iigsBlitRectInner(dstRow0, (uint16_t)dstX,
srcRow0, (uint16_t)srcX,
(uint16_t)copyW, (uint16_t)copyH,
(uint16_t)srcRowBytes,
transparent);
return true;
}
bool halFastTileFill(SurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
uint8_t *row;
uint16_t pixelX;
uint16_t pixelY;
if (s == NULL) {
return false;
}
pixelX = (uint16_t)((uint16_t)bx * 8u);
pixelY = (uint16_t)((uint16_t)by * 8u);
row = &s->pixels[pixelY * SURFACE_BYTES_PER_ROW + (pixelX >> 1)];
iigsTileFillInner(row, fillWord);
return true;
}
uint8_t *halStageAllocPixels(void) {
return IIGS_STAGE_PIXELS;
}
void halStageFreePixels(uint8_t *pixels) {
(void)pixels;
// Backing memory is hardware-pinned; nothing to free.
}
// $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active // $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active
// scan. To produce a rising-edge wait (one VBL per call), first spin // scan. To produce a rising-edge wait (one VBL per call), first spin
// while VBL is currently active (bit 7 = 0), then spin until VBL // while VBL is currently active (bit 7 = 0), then spin until VBL
@ -146,12 +543,3 @@ void halWaitVBL(void) {
/* scanning: wait for next VBL */; /* scanning: wait for next VBL */;
} }
} }
void halShutdown(void) {
if (gModeSet) {
*IIGS_NEWVIDEO_REG = gPreviousNewVideo;
*IIGS_BORDER_REG = gPreviousBorder;
gModeSet = false;
}
}

3865
src/port/iigs/joeyDraw.asm Normal file

File diff suppressed because it is too large Load diff

76
src/port/iigs/peislam.asm Normal file
View file

@ -0,0 +1,76 @@
* peislam.asm - PEI-slam stage row to bank-$E1 SHR.
*
* Implements the //e AUXWRITE + RAMRD + SHR-shadow trick that lets
* 65816 stack pushes (which are bank-$00-implicit) end up in bank
* $E1 SHR display memory:
*
* - SHR shadow temporarily ENABLED (clear $C035 bit 3) so writes
* to bank-$01 in $2000-$9FFF mirror to $E1 SHR.
* - AUXWRITE on (any write to $C005) so bank-$00 stack writes
* redirect to bank $01, then mirror to $E1 via shadow.
* - RAMRD on (any write to $C003) so PEI dp's bank-$00-implicit
* reads redirect to bank $01 = the stage source.
* - SEI for the duration: stack pointer is hijacked to point at
* $E1-mapped stack space, soft-switch state would corrupt any
* C code that tried to access bank-$00 globals.
*
* All scratch reads/writes within the slam use long-mode `>name`
* addressing (24-bit, explicit bank) so they bypass RAMRD redirect
* and reach the actual bank-$00 global storage.
*
* Calling convention: ORCA-C memory model 1 (large model, JSL/RTL).
* void peiSlamFullRow(int16_t y);
* - Caller PHAs y (2 bytes) before JSL.
* - JSL pushes 3-byte return address.
* - On entry: y_LO at SP+4, y_HI at SP+5 (SP points one below PCL).
* - Function preserves DBR; returns via RTL with original SP.
* - Caller pops the y arg after RTL.
*
* Per call: ~50 cyc bracket + 80 PEIs * 6 cyc = ~530 cyc, vs the
* memcpy/MVN fallback's 7 cyc/byte * 160 bytes = ~1120 cyc.
keep PEISLAM
case on
* The operand to START names the LOAD segment this object segment
* belongs to (per ORCA/M for IIgs manual, ch. 6 "Load Segments").
* Object segments without an operand land in the unnamed "blank
* segment" -- which on AUDIO is _ROOT, the very segment whose 64 KB
* budget peislam.asm was busting. Naming a load segment forces the
* linker to put us in our own segment, which the GS/OS loader then
* allocates in its own bank.
peiSlamFullRow start IIGSASM
* MVN-based row copy. Replaces the PEI-stack-slam approach (which
* needs RAMRD/AUXWRITE/SHADOW soft-switches and is sensitive to
* DRAWDATA bank placement). MVN copies 160 bytes from the bank-$01
* stage row to the matching bank-$E1 SHR row at ~7 cyc/byte; that's
* slower than PEI-slam but rock-solid.
*
* Args after PHP: y (int16) at SP+5..6. Compute rowOffset = $2000
* + y*160. MVN $01,$E1 with X=Y=rowOffset, A=159 copies 160 bytes
* from $01:rowOffset to $E1:rowOffset.
php
rep #$30 ; M=16, X=16
lda 5,s ; y
asl a
asl a
asl a
asl a
asl a ; A = y << 5 = y*32
sta >gPeiTempRowBase
asl a
asl a ; A = y << 7 = y*128
clc
adc >gPeiTempRowBase ; A = y*160
clc
adc #$2000 ; A = $2000 + y*160 = row offset
tax ; X = source offset (bank $01)
tay ; Y = dest offset (bank $E1)
lda #159 ; count - 1 (MVN copies count+1 = 160 bytes)
mvn $01,$E1
plp
rtl
end

View file

@ -1071,6 +1071,7 @@ EOF
fi fi
} }
install_gsplus() { install_gsplus() {
local base="${SCRIPT_DIR}/emulators/gsplus" local base="${SCRIPT_DIR}/emulators/gsplus"
local bin="${base}/bin/gsplus" local bin="${base}/bin/gsplus"