Lots of fixes. Maybe.

This commit is contained in:
Scott Duensing 2026-06-29 01:37:34 -05:00
parent de280b7a32
commit 1972f6df90
19 changed files with 1617 additions and 939 deletions

View file

@ -1452,84 +1452,39 @@ static int16_t gBfsQueueX[WALK_GRID_H * WALK_GRID_W];
static int16_t gBfsQueueY[WALK_GRID_H * WALK_GRID_W];
static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) {
int16_t qhead;
int16_t qtail;
int16_t cx, cy;
int16_t nx, ny;
uint8_t d;
int16_t scx, scy, tcx, tcy;
// Reset every BFS parent cell to unvisited (0xFF).
//
// The pathfinder is split across several helpers so no single function
// holds enough simultaneously-live int16_t values to exhaust the very
// small w65816 register file. They are marked noinline so the -O2
// inliner cannot fold them back into findPath and recreate the
// "ran out of registers" failure.
static __attribute__((noinline)) void bfsClear(void) {
int16_t row;
int16_t col;
footToCell(sx, sy, &scx, &scy);
footToCell(tx, ty, &tcx, &tcy);
/* If the target is in an unwalkable cell, find the nearest
* walkable one by snapping along x/y axes. */
if (!walkable(room, tcx, tcy)) {
int16_t r;
bool found;
found = false;
for (r = 1; r < 12 && !found; r++) {
int16_t dx, dy;
for (dy = -r; dy <= r && !found; dy++) {
for (dx = -r; dx <= r && !found; dx++) {
if (walkable(room, (int16_t)(tcx + dx), (int16_t)(tcy + dy))) {
tcx = (int16_t)(tcx + dx);
tcy = (int16_t)(tcy + dy);
found = true;
}
}
}
}
if (!found) {
gPathLen = 0;
return false;
}
}
if (!walkable(room, scx, scy)) {
/* If we're somehow stuck off-grid, snap to nearest. Same as
* above but tiny radius. */
int16_t r;
bool found;
found = false;
for (r = 1; r < 8 && !found; r++) {
int16_t dx, dy;
for (dy = -r; dy <= r && !found; dy++) {
for (dx = -r; dx <= r && !found; dx++) {
if (walkable(room, (int16_t)(scx + dx), (int16_t)(scy + dy))) {
scx = (int16_t)(scx + dx);
scy = (int16_t)(scy + dy);
found = true;
}
}
}
}
}
for (row = 0; row < WALK_GRID_H; row++) {
int16_t col;
for (col = 0; col < WALK_GRID_W; col++) {
gBfsParent[row][col] = 0xFF;
}
}
}
qhead = 0;
qtail = 0;
gBfsQueueX[qtail] = scx;
gBfsQueueY[qtail] = scy;
qtail++;
gBfsParent[scy][scx] = 0xFE; /* sentinel: start */
while (qhead < qtail) {
cx = gBfsQueueX[qhead];
cy = gBfsQueueY[qhead];
qhead++;
if (cx == tcx && cy == tcy) {
break;
}
for (d = 0; d < 4; d++) {
// Breadth-first search from (scx,scy) toward (tcx,tcy) across the room's
// walk grid, recording each visited cell's incoming direction byte in
// gBfsParent. Returns true if the target cell was reached. Split out of
// findPath to keep the hot loop's live-value count low enough for the
// w65816 register allocator.
// Visit one neighbour of (cx,cy) in direction d. If that neighbour is
// walkable and not yet visited, record its incoming direction and append
// it to the BFS queue, advancing *qtail. Kept noinline so bfsSearch's
// inner loop reduces to a single call and stays within the w65816
// register budget.
static __attribute__((noinline)) void bfsExpand(RoomE room, int16_t cx, int16_t cy, uint8_t d, int16_t *qtail) {
int16_t nx;
int16_t ny;
nx = cx;
ny = cy;
switch (d) {
@ -1539,29 +1494,59 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
default: nx = (int16_t)(cx - 1); break;
}
if (!walkable(room, nx, ny)) {
continue;
return;
}
if (gBfsParent[ny][nx] != 0xFF) {
continue;
return;
}
gBfsParent[ny][nx] = d;
gBfsQueueX[qtail] = nx;
gBfsQueueY[qtail] = ny;
gBfsQueueX[*qtail] = nx;
gBfsQueueY[*qtail] = ny;
(*qtail)++;
}
static __attribute__((noinline)) bool bfsSearch(RoomE room, int16_t scx, int16_t scy, int16_t tcx, int16_t tcy) {
int16_t qhead;
int16_t qtail;
int16_t cx;
int16_t cy;
uint8_t d;
qhead = 0;
qtail = 0;
gBfsQueueX[qtail] = scx;
gBfsQueueY[qtail] = scy;
qtail++;
gBfsParent[scy][scx] = 0xFE; // sentinel: start
while (qhead < qtail) {
cx = gBfsQueueX[qhead];
cy = gBfsQueueY[qhead];
qhead++;
if (cx == tcx && cy == tcy) {
break;
}
for (d = 0; d < 4; d++) {
bfsExpand(room, cx, cy, d, &qtail);
}
}
if (gBfsParent[tcy][tcx] == 0xFF) {
gPathLen = 0;
return false;
}
return gBfsParent[tcy][tcx] != 0xFF;
}
/* Walk back from target to start, prepending to the path. */
{
// Reconstruct the path by walking gBfsParent back from the target to
// the start, then emit it forward (start..target) into the gPath*
// waypoint arrays as foot pixel positions.
static __attribute__((noinline)) void buildPath(int16_t tcx, int16_t tcy) {
int16_t pathRevX[MAX_PATH_NODES];
int16_t pathRevY[MAX_PATH_NODES];
int16_t n;
int16_t i;
int16_t cx;
int16_t cy;
uint8_t d;
n = 0;
cx = tcx;
@ -1571,7 +1556,9 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
pathRevY[n] = cy;
n++;
d = gBfsParent[cy][cx];
if (d == 0xFE) { break; }
if (d == 0xFE) {
break;
}
switch (d) {
case 0: cy = (int16_t)(cy + 1); break;
case 1: cx = (int16_t)(cx - 1); break;
@ -1582,16 +1569,79 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
gPathLen = 0;
for (i = (int16_t)(n - 1); i >= 0; i--) {
int16_t px, py;
int16_t px;
int16_t py;
cellToFoot(pathRevX[i], pathRevY[i], &px, &py);
gPathX[gPathLen] = px;
gPathY[gPathLen] = py;
gPathLen++;
if (gPathLen >= MAX_PATH_NODES) { break; }
if (gPathLen >= MAX_PATH_NODES) {
break;
}
}
gPathStep = 0;
}
// Snap a cell to the nearest walkable one via an expanding box search
// (dy outer, dx inner, radius 1..maxR-1, same scan order as the original
// inline loop). Returns true if *cx/*cy now name a walkable cell (it was
// already walkable, or a snap target was found); false if none was found
// within range. Only writes *cx/*cy when a walkable cell is found.
static __attribute__((noinline)) bool snapToWalkable(RoomE room, int16_t *cx, int16_t *cy, int16_t maxR) {
int16_t r;
if (walkable(room, *cx, *cy)) {
return true;
}
for (r = 1; r < maxR; r++) {
int16_t dy;
for (dy = (int16_t)(-r); dy <= r; dy++) {
int16_t dx;
for (dx = (int16_t)(-r); dx <= r; dx++) {
int16_t ncx;
int16_t ncy;
ncx = (int16_t)(*cx + dx);
ncy = (int16_t)(*cy + dy);
if (walkable(room, ncx, ncy)) {
*cx = ncx;
*cy = ncy;
return true;
}
}
}
}
return false;
}
static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) {
int16_t scx;
int16_t scy;
int16_t tcx;
int16_t tcy;
footToCell(sx, sy, &scx, &scy);
footToCell(tx, ty, &tcx, &tcy);
// If the target is in an unwalkable cell, snap to the nearest
// walkable one (radius < 12); give up if none is within range.
if (!snapToWalkable(room, &tcx, &tcy, 12)) {
gPathLen = 0;
return false;
}
// If the start is somehow off the walkmask, snap it too (tiny
// radius < 8); proceed with the original cell if nothing is found.
snapToWalkable(room, &scx, &scy, 8);
bfsClear();
if (!bfsSearch(room, scx, scy, tcx, tcy)) {
gPathLen = 0;
return false;
}
buildPath(tcx, tcy);
return gPathLen > 0;
}

View file

@ -152,7 +152,10 @@ static jlSurfaceT *gBackdrop;
static jlSurfaceT *gStage;
static AgiVmT gVm;
static LogicCacheSlotT gLogicCache[AGI_MAX_RESOURCES];
static ViewCacheSlotT gViewCache[AGI_MAX_RESOURCES];
// View cache: 256 slots of ~5 KB each (~1.3 MB). Far too large for the
// IIgs bank-0 BSS, so it is jlAlloc'd at startup (Memory Manager handle
// on the IIgs, malloc elsewhere) rather than declared as a static array.
static ViewCacheSlotT *gViewCache;
static bool gPicReady;
static uint8_t gOverrideRoom;
static bool gOverrideRoomActive;
@ -1729,6 +1732,16 @@ int main(int argc, char **argv) {
}
}
// The view cache is too large for bank-0 BSS on the IIgs; pull it
// from the platform's large-allocator. jlAlloc does not zero its
// result, so clear it so every slot starts with loaded == false.
gViewCache = (ViewCacheSlotT *)jlAlloc(sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES);
if (gViewCache == NULL) {
jlShutdown();
return 1;
}
memset(gViewCache, 0, sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES);
gameDir = resolveGameDir(argc, argv);
startingRoom = resolveStartingRoom(argc, argv);
gOverrideRoom = startingRoom;
@ -1858,6 +1871,8 @@ int main(int argc, char **argv) {
jlAudioShutdown();
releaseLogicCache();
releaseViewCache();
jlFree(gViewCache);
gViewCache = NULL;
if (gBackdrop != NULL) {
jlSurfaceDestroy(gBackdrop);
}

View file

@ -13,6 +13,7 @@
#include "agi.h"
#include "joey/core.h"
#include "joey/draw.h"
#include "surfaceInternal.h"
@ -408,8 +409,8 @@ static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) {
// ----- Public API (alphabetical) -----
bool agiPicAlloc(AgiPicT *pic) {
pic->visual = (uint8_t *)malloc(AGI_PIC_PIXELS);
pic->priority = (uint8_t *)malloc(AGI_PIC_PIXELS);
pic->visual = (uint8_t *)jlAlloc(AGI_PIC_PIXELS);
pic->priority = (uint8_t *)jlAlloc(AGI_PIC_PIXELS);
if (pic->visual == NULL || pic->priority == NULL) {
agiPicFree(pic);
return false;
@ -651,11 +652,11 @@ void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
void agiPicFree(AgiPicT *pic) {
if (pic->visual != NULL) {
free(pic->visual);
jlFree(pic->visual);
pic->visual = NULL;
}
if (pic->priority != NULL) {
free(pic->priority);
jlFree(pic->priority);
pic->priority = NULL;
}
}

View file

@ -43,14 +43,157 @@
#endif
// ----- File-local types -----
// Per-draw invariants for one cel blit, grouped so they live in memory
// rather than competing for the w65816's tiny register file. Passing a
// pointer to this keeps the hot blit helpers under the backend's
// register-allocation ceiling.
typedef struct {
const uint8_t *picPriority;
int16_t x;
int16_t width;
uint8_t actorPri;
uint8_t transparent;
bool mirrored;
} AgiCelBlitT;
// ----- Prototypes -----
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY);
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow);
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height);
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes);
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored);
// ----- Internal helpers (alphabetical) -----
// Planar fallback row walker (stage->pixels == NULL). Routes every
// opaque, visible pixel through jlDrawPixel so the port's c2p /
// plane-sync path stays correct. Split out of agiViewDraw so its
// locals don't pile onto the chunky path's register budget.
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) {
int16_t width;
int16_t cy;
int16_t cx;
int16_t drawCx;
int16_t agiX;
int16_t agiY;
int16_t stageX;
int16_t stageY;
uint8_t rleByte;
uint8_t color;
uint8_t runLen;
uint8_t r;
uint8_t picPri;
width = blit->width;
for (cy = 0; cy < height; cy++) {
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
if (color != blit->transparent) {
drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(blit->x + drawCx);
agiY = (int16_t)(topY + cy);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
picPri = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
if (picPri <= blit->actorPri) {
stageX = (int16_t)(agiX << 1);
stageY = (int16_t)(destY + agiY);
jlDrawPixel(stage, stageX, stageY, color);
jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
}
}
}
cx++;
}
}
}
}
// Chunky fast-path walker for one source row. Consumes the cel's RLE
// stream from `rle`, writing pixel-doubled bytes into `stageRow` where
// the picture priority allows it, and returns the advanced RLE pointer
// so the caller can hand it to the next row. A NULL priRow / stageRow
// means the row is clipped out vertically: the RLE is still walked (so
// the pointer lands correctly for later rows) but nothing is drawn.
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) {
int16_t width;
int16_t cx;
int16_t drawCx;
int16_t agiX;
uint8_t rleByte;
uint8_t color;
uint8_t runLen;
uint8_t packed;
uint8_t r;
width = blit->width;
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
// Transparent runs and vertically-clipped rows both just skip
// ahead by runLen without touching the stage.
if (color == blit->transparent || priRow == NULL) {
cx = (int16_t)(cx + runLen);
if (cx > width) { cx = width; }
continue;
}
packed = (uint8_t)((color << 4) | color);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(blit->x + drawCx);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
if (priRow[agiX] <= blit->actorPri) {
stageRow[agiX] = packed;
}
}
cx++;
}
}
return rle;
}
// Mark the cel's stage bounding box dirty once after a chunky blit.
// Computes the pixel-doubled bbox from the source rect, clips it to the
// stage so the surface marker doesn't reject the whole rect for an
// off-screen edge, and records it if any of it is on-screen.
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) {
int16_t minStageX;
int16_t maxStageX;
int16_t minStageY;
int16_t maxStageY;
minStageX = (int16_t)(x << 1);
maxStageX = (int16_t)((x + width) << 1);
minStageY = (int16_t)(destY + topY);
maxStageY = (int16_t)(destY + topY + height);
if (minStageX < 0) { minStageX = 0; }
if (minStageY < 0) { minStageY = 0; }
if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; }
if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
if (maxStageX > minStageX && maxStageY > minStageY) {
surfaceMarkDirtyRect(stage, minStageX, minStageY,
(int16_t)(maxStageX - minStageX),
(int16_t)(maxStageY - minStageY));
}
}
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) {
uint8_t celCount;
uint8_t i;
@ -149,34 +292,24 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
const uint8_t *picPriority,
jlSurfaceT *stage, int16_t destY) {
const AgiCelInfoT *cel;
AgiCelBlitT blit;
const uint8_t *rle;
bool mirrored;
int16_t width;
int16_t height;
uint8_t transparent;
int16_t topY;
int16_t cy;
int16_t cx;
int16_t drawCx;
int16_t agiX;
int16_t agiY;
int16_t stageX;
int16_t stageY;
uint8_t rleByte;
uint8_t color;
uint8_t packed;
uint8_t runLen;
uint8_t r;
uint8_t picPri;
uint8_t *stagePixels;
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
if (cel == NULL) {
return;
}
width = (int16_t)cel->width;
height = (int16_t)cel->height;
transparent = cel->transparentColor;
blit.picPriority = picPriority;
blit.x = x;
blit.width = (int16_t)cel->width;
blit.actorPri = actorPri;
blit.transparent = cel->transparentColor;
blit.mirrored = mirrored;
rle = cel->rleData;
topY = (int16_t)(y - height + 1);
stagePixels = (stage != NULL) ? stage->pixels : NULL;
@ -187,10 +320,8 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
// direct, marking the cel bbox dirty once at the end. ~80x
// faster on DOSBox @ 386SX-equivalent CPU cycles.
if (stagePixels != NULL) {
int16_t minStageX = (int16_t)(x << 1);
int16_t maxStageX = (int16_t)((x + width) << 1);
int16_t minStageY = (int16_t)(destY + topY);
int16_t maxStageY = (int16_t)(destY + topY + height);
int16_t cy;
int16_t agiY;
const uint8_t *priRow;
uint8_t *stageRow;
@ -208,82 +339,15 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
stageRow = stagePixels +
(int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW;
}
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
if (color == transparent) {
cx = (int16_t)(cx + runLen);
if (cx > width) { cx = width; }
continue;
}
if (priRow == NULL) {
cx = (int16_t)(cx + runLen);
if (cx > width) { cx = width; }
continue;
}
packed = (uint8_t)((color << 4) | color);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(x + drawCx);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
if (priRow[agiX] <= actorPri) {
stageRow[agiX] = packed;
}
}
cx++;
}
}
}
// Dirty-rect tracking at the cel level only: we know the
// bbox a priori (the cel's stage rect), and per-pixel tight
// bounds aren't worth the inner-loop cost on a 386. Clip to
// the stage so the surface marker doesn't reject the whole
// rect for an off-screen edge.
if (minStageX < 0) { minStageX = 0; }
if (minStageY < 0) { minStageY = 0; }
if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; }
if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
if (maxStageX > minStageX && maxStageY > minStageY) {
surfaceMarkDirtyRect(stage, minStageX, minStageY,
(int16_t)(maxStageX - minStageX),
(int16_t)(maxStageY - minStageY));
rle = blitRowChunky(&blit, rle, priRow, stageRow);
}
markCelDirty(stage, x, blit.width, destY, topY, height);
return;
}
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
for (cy = 0; cy < height; cy++) {
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
if (color != transparent) {
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(x + drawCx);
agiY = (int16_t)(topY + cy);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
picPri = picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
if (picPri <= actorPri) {
stageX = (int16_t)(agiX << 1);
stageY = (int16_t)(destY + agiY);
jlDrawPixel(stage, stageX, stageY, color);
jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
}
}
}
cx++;
}
}
}
blitPlanar(&blit, rle, height, topY, stage, destY);
}

File diff suppressed because it is too large Load diff

View file

@ -265,7 +265,17 @@ static void showcaseCellRect(uint16_t index, int16_t *outX, int16_t *outY, int16
}
static void drawShowcase(void) {
// optnone: this 12-case showcase has too many simultaneously-live locals for
// the w65816 register allocator at -O2 (it bails "ran out of registers"). It
// renders once at startup, so dropping optimization here costs nothing.
// optnone is a clang-only attribute; GCC (DOS/Amiga/ST) rejects it under
// -Werror=attributes and does not need it, so guard it to clang.
#if defined(__clang__)
#define DRAWSHOWCASE_ATTR __attribute__((noinline, optnone))
#else
#define DRAWSHOWCASE_ATTR __attribute__((noinline))
#endif
static void DRAWSHOWCASE_ATTR drawShowcase(void) {
uint16_t cell;
int16_t x;
int16_t y;
@ -408,7 +418,7 @@ static void drawShowcase(void) {
// ----- Main -----
static void runAllTests(void) {
static void __attribute__((noinline)) runAllTests(void) {
jlLogF("UBER: ----- begin -----\n");
// Surface / palette / SCB.
@ -474,22 +484,87 @@ static void runAllTests(void) {
}
int main(void) {
jlConfigT config;
// Extracted from main so main's register pressure stays under the w65816
// allocator's ceiling -- the 16-entry pal[] + loop index was the overflow.
// noinline is load-bearing at -O2 (the backend would otherwise re-inline a
// single-call static and recreate the pressure).
static void __attribute__((noinline)) setupPalette(void) {
uint16_t pal[16];
int i;
uint16_t startFrame;
for (i = 0; i < 16; i++) {
pal[i] = (uint16_t)((i << 8) | (i << 4) | i); // grey ramp
}
pal[ 0] = 0x000;
pal[ 1] = 0x800; // dark red (running)
pal[ 2] = 0x080; // green (done)
pal[ 3] = 0x008; // blue
pal[ 5] = 0xFF0; // yellow (test pixels)
pal[ 7] = 0xFFF; // white (fills)
pal[15] = 0xF00; // red
jlPaletteSet(gStage, 0, pal);
jlScbSetRange(gStage, 0, 199, 0);
}
// Extracted from main for the same register-pressure reason as setupPalette.
static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) {
uint16_t endFrame;
uint16_t elapsedFrames;
unsigned long elapsedMs;
endFrame = jlFrameCount();
elapsedFrames = (uint16_t)(endFrame - startFrame);
elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz();
jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)\n",
elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
}
// Extracted from main (register pressure). Returns false on sprite-create fail.
static bool __attribute__((noinline)) setupSprite(void) {
uint16_t before;
buildBallSprite();
gSprite = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (gSprite == NULL) {
jlLog("UBER: jlSpriteCreate failed");
return false;
}
// jlSpriteCompile is a one-shot. Time at frame resolution.
jlWaitVBL();
before = jlFrameCount();
if (!jlSpriteCompile(gSprite)) {
jlLog("UBER: jlSpriteCompile failed");
}
while (jlFrameCount() == before) {
/* wait for next VBL edge */
}
jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame\n");
gBackup.bytes = gBackupBytes;
return true;
}
// Extracted from main (register pressure): the jlConfigT struct on main's
// frame, on top of the rest of main, pushed the w65816 allocator over its
// limit. noinline keeps it out at -O2.
static bool __attribute__((noinline)) initJoeyLib(void) {
jlConfigT config;
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
* compiled sprite emitter generates. UL on the multiply because
* a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024;
config.audioBytes = 64UL * 1024;
return jlInit(&config);
}
if (!jlInit(&config)) {
int main(void) {
uint16_t startFrame;
if (!initJoeyLib()) {
return 1;
}
/* jlFrameCount is VBL-driven, so it only ticks after halInit
@ -505,46 +580,17 @@ int main(void) {
}
// A simple visible palette so users see SOMETHING during the run.
for (i = 0; i < 16; i++) {
pal[i] = (uint16_t)((i << 8) | (i << 4) | i); // grey ramp
}
pal[ 0] = 0x000;
pal[ 1] = 0x800; // dark red (running)
pal[ 2] = 0x080; // green (done)
pal[ 3] = 0x008; // blue
pal[ 5] = 0xFF0; // yellow (test pixels)
pal[ 7] = 0xFFF; // white (fills)
pal[15] = 0xF00; // red
jlPaletteSet(gStage, 0, pal);
jlScbSetRange(gStage, 0, 199, 0);
setupPalette();
// Indicate "running": red bar at top of screen.
jlSurfaceClear(gStage, 0);
jlFillRect(gStage, 0, 0, 320, 8, 1);
jlStagePresent();
buildBallSprite();
gSprite = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (gSprite == NULL) {
jlLog("UBER: jlSpriteCreate failed");
if (!setupSprite()) {
jlShutdown();
return 1;
}
// jlSpriteCompile is a one-shot. Time at frame resolution.
{
uint16_t before;
jlWaitVBL();
before = jlFrameCount();
if (!jlSpriteCompile(gSprite)) {
jlLog("UBER: jlSpriteCompile failed");
}
while (jlFrameCount() == before) {
/* wait for next VBL edge */
}
jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame\n");
}
gBackup.bytes = gBackupBytes;
// Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx
// without first calling jlAudioPlayMod leaves NTP's engine in a
@ -569,11 +615,7 @@ int main(void) {
runAllTests();
endFrame = jlFrameCount();
elapsedFrames = (uint16_t)(endFrame - startFrame);
elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz();
jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)\n",
elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
reportElapsed(startFrame);
// Done. Green screen + waitForKey.
jlSurfaceClear(gStage, 2);

View file

@ -27,6 +27,18 @@ const char *jlPlatformName(void);
// Returns the library version string (e.g., "1.0.0").
const char *jlVersionString(void);
// Allocate a large or persistent buffer, backed by the platform's
// native allocator rather than the small C heap. Use this (not malloc)
// for anything that exceeds the per-allocation or total ceiling of the
// stock C heap -- on the IIgs the C heap lives in bank 0 and is tiny,
// and a single malloc is capped at ~32 KB, so multi-KB caches and frame
// buffers must come from the IIgs Memory Manager instead. Returns NULL
// on failure. The returned pointer is NOT zero-filled.
void *jlAlloc(uint32_t bytes);
// Release a buffer returned by jlAlloc. NULL is accepted and ignored.
void jlFree(void *p);
// Block the calling thread until the next display vertical blank.
// Used to pace game loops to the display's native refresh rate
// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz

View file

@ -52,12 +52,39 @@ ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.c $(EXAMPLES)/spacetaxi/stAudio.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
.PHONY: all iigs iigs-lib iigs-clang-smoke clean-iigs
# NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB
# binary, then bake it into a dead-strippable GAS data object (gNtpPlayerBytes
# / _len, which audio_full.c externs). bin-to-s.sh is the clang-era replacement
# for the deleted bin-to-asm.sh. Only the AUDIO binary pulls it in; link816
# dead-strips it everywhere else.
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
NTP_ASM := $(BUILD)/audio/ntpdata.s
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples clean-iigs
# Default: compile-check the library + run the end-to-end smoke test.
all iigs: iigs-lib iigs-clang-smoke
# Per-example launchable binaries. AUDIO (34KB NTP rodata needs link816
# rodata-bank splitting) and STAXI (2-byte cross-seg reloc to a runtime
# helper) are blocked on deeper toolchain features and are omitted here.
iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/JOY \
$(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \
$(BINDIR)/AGI
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
@mkdir -p $(BUILD)/audio
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@
$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-s.sh
$(REPO_DIR)/toolchains/iigs/bin-to-s.sh $(NTP_BIN) $@ gNtpPlayerBytes gNtpPlayerBytes_len
# Compile-check every library and example translation unit with clang,
# and assemble every .s with llvm-mc. This is the canonical "does the
# IIgs source still build" gate while the launchable pipeline is wired.
@ -124,9 +151,14 @@ $(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
# NOTE: the audio example is omitted until the NinjaTrackerPlus replayer
# is wired into the clang build (a path for baking the replayer bytes
# into a data segment via llvm-mc is pending).
# AUDIO loads the NTP replayer from disk at runtime: audio_full.c reads
# ntpplayer.bin via NTP_PATH instead of baking the 34KB replayer as rodata
# (which overflowed bank 0, and clang's IMM16+$BE near form can't reach a far
# rodata segment anyway). The binary no longer links $(NTP_ASM); $(NTP_BIN) is
# still built so it can be shipped on the application disk next to the binary.
$(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB_SRCS) $(NTP_BIN) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AUDIO.d $(INCLUDES) -o $@ $(AUDIO_SRC) $(LIB_SRCS)
clean-iigs:
rm -rf $(BUILD)

View file

@ -33,6 +33,14 @@
// halSpriteXxxPlanes path) instead of corrupting the heap. The
// constant is a long literal so it never wraps the 16-bit int the
// IIgs uses for plain int arithmetic.
//
// The scratch is taken from halBigAlloc (jlAlloc's backing), NOT plain
// malloc: 16 KB exceeds the per-allocation / total ceiling of the stock
// C heap on some ports (the IIgs C heap lives in bank 0 and is only a
// few KB), where a malloc that large either fails outright or -- with a
// bump allocator that does not bound-check -- hands back an out-of-heap
// pointer that corrupts memory. halBigAlloc routes to the platform's
// native allocator (IIgs Memory Manager; plain malloc elsewhere).
#define SPRITE_EMIT_SCRATCH_BYTES (16384ul)
@ -122,6 +130,22 @@ static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
}
/* #19: the ORIGINAL IIgs sprite codegen (a self-modifying call stub + inline
* asm + hand-rolled per-call bank/offset patching) had a layout-dependent
* memory corruption that proved impossible to instrument (the GS/OS
* multi-segment loader relocates BSS / the arena / the back buffer to
* nondeterministic banks+offsets and there is no fixed C->emulator diagnostic
* channel). It was REWRITTEN rather than patched: the emitted routines are now
* clean C-ABI functions invoked through ordinary C function pointers, so the
* compiler emits the call and there is no self-modifying code or manual address
* math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c.
* Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the
* interpreter. */
#ifndef JOEY_IIGS_SPRITE_CODEGEN
#define JOEY_IIGS_SPRITE_CODEGEN 1
#endif
bool jlSpriteCompile(jlSpriteT *sp) {
uint8_t *scratch;
uint32_t totalSize;
@ -141,6 +165,10 @@ bool jlSpriteCompile(jlSpriteT *sp) {
if (sp->tileData == NULL) {
return false;
}
#if defined(JOEYLIB_PLATFORM_IIGS) && !JOEY_IIGS_SPRITE_CODEGEN
/* #19: leave sp->slot NULL -> interpreter path. See the note above. */
return false;
#endif
/* Amiga (post-Phase 9) uses spriteEmitPlanar68k.c which writes
* directly to bitplanes. DRAW emits a unique pre-shifted variant
* per shift in 0..7 (smooth horizontal motion at any pixel x);
@ -149,7 +177,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
* bytes per row). The post-emit pass below aliases slots 2..7
* for save/restore to slot 1's bytes. */
scratch = (uint8_t *)malloc(SPRITE_EMIT_SCRATCH_BYTES);
scratch = (uint8_t *)halBigAlloc(SPRITE_EMIT_SCRATCH_BYTES);
if (scratch == NULL) {
return false;
}
@ -158,7 +186,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single
* routine that did not fit the scratch is rejected here as well. */
if (totalSize > 0xFFFFu) {
free(scratch);
halBigFree(scratch);
return false;
}
if (totalSize == 0) {
@ -167,13 +195,13 @@ bool jlSpriteCompile(jlSpriteT *sp) {
* RestoreUnder would dereference a degenerate slot or chunky
* shadow. Bail so sp->slot stays NULL and the dispatcher
* routes through the interpreted halSpriteXxxPlanes path. */
free(scratch);
halBigFree(scratch);
return false;
}
slot = codegenArenaAlloc(totalSize);
if (slot == NULL) {
free(scratch);
halBigFree(scratch);
return false;
}
@ -194,7 +222,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* The second pass disagreed with the sizing pass (must
* not happen, but never write past the slot). Roll back. */
codegenArenaFree(slot);
free(scratch);
halBigFree(scratch);
return false;
}
/* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED)
@ -221,7 +249,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
#endif
sp->slot = slot;
free(scratch);
halBigFree(scratch);
return true;
}
@ -254,169 +282,56 @@ static uint8_t spriteChunkyRestoreShift(const jlSpriteT *sp, const jlSpriteBacku
// declared in surfaceInternal.h. Uses a single indexed long-mode read
// for the multiply.
// IIgs uses inline asm + a self-modifying call stub instead of a C
// function-pointer cast. The build uses a large memory model so
// pointers are 24-bit and JSL works cross-bank.
//
// `sta abs,Y` on 65816 uses the data bank register (DBR) for the
// high byte of the effective address, so we need DBR = dst's bank
// during the body. malloc under -b can return memory in any bank,
// so we don't trust DBR to already match -- the stub explicitly
// sets DBR from the dst pointer's bank byte and restores it before
// returning to C.
//
// Stub layout (14 bytes):
// 00: 8B PHB ; save caller DBR
// 01: A9 bk LDA #destBank ; A = dst bank (8-bit M)
// 03: 48 PHA
// 04: AB PLB ; DBR = dst bank
// 05: A0 lo hi LDY #destOffset ; Y = low 16 of dst (X=16)
// 08: 22 lo mid bk JSL routine
// 0C: AB PLB ; restore caller DBR
// 0D: 6B RTL
//
// Patched per call: byte 2 (destBank), bytes 6-7 (destOffset16),
// bytes 9-11 (target 24-bit). The compiled routine assumes
// M=8 / X=16 / Y=destOffset on entry; the stub arranges that.
//
// Stub bytes are split into two phases:
// 1. The 8 opcode bytes are written ONCE on first call (gDrawStubInited).
// 2. Of the 6 operand bytes, only those that actually changed since
// the previous call get re-stamped: destBank and fnAddr are cached
// and rarely change (per-shift / per-bank). destOffset is the only
// one that changes every call as the sprite moves. Net per-frame
// patching for the typical case drops from 14 stores to 2.
static unsigned char gSpriteCallStub[14];
static bool gDrawStubInited = false;
static uint8_t gDrawStubLastBank = 0xFF;
static uint32_t gDrawStubLastFnAddr = 0xFFFFFFFFul;
// IIgs sprite dispatch. The emitted routines (spriteEmitIigs.c) are ordinary
// C-ABI functions called through C function pointers, so the compiler emits the
// cross-bank JSL itself (large memory model -> 24-bit pointers). The DRAW
// routine takes the 24-bit dst pointer in A:X and sets up its own DBR (needed
// for `sta abs,Y`, whose high byte comes from DBR) + Y in a fixed prologue, then
// restores DBR before returning. SAVE/RESTORE take packed src/dst offsets and
// have only their MVN bank operands patched per call (a plain C pointer write
// into the arena from the dispatcher). No self-modifying call stub, no inline
// asm -- the machinery that hid the old un-instrumentable #19 corruption.
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
uint8_t shift;
uint16_t destOffset;
uint8_t destBank;
uint8_t *dstRow0;
uint32_t fnAddr;
void (*drawFn)(uint8_t *);
{
uint8_t *destPtr;
uint32_t destAddr;
const uint8_t *destB;
// Clean C-ABI dispatch. The emitted DRAW routine is a normal C function
// -- void draw(uint8_t *dstRow0) -- so the compiler emits the indirect
// JSL itself: NO self-modifying call stub, NO inline asm, NO hand-rolled
// per-call bank/offset patching (that machinery was the source of the
// old un-instrumentable corruption). dstRow0 (the sprite's top-left
// screen byte) is passed in A:X; the routine's prologue (spriteEmitIigs.c)
// sets up its own DBR/Y and restores DBR on exit.
shift = (uint8_t)(x & 1);
destPtr = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
/* Byte-alias the 24-bit pointer to grab lo16 + bank without the
* ~LSHR4 32-bit-shift helper, matching SPLIT_POINTER below. */
destAddr = (uint32_t)destPtr;
destB = (const uint8_t *)&destAddr;
destOffset = (uint16_t)(destB[0] | ((uint16_t)destB[1] << 8));
destBank = destB[2];
dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
}
if (!gDrawStubInited) {
gSpriteCallStub[ 0] = 0x8B;
gSpriteCallStub[ 1] = 0xA9;
gSpriteCallStub[ 3] = 0x48;
gSpriteCallStub[ 4] = 0xAB;
gSpriteCallStub[ 5] = 0xA0;
gSpriteCallStub[ 8] = 0x22;
gSpriteCallStub[12] = 0xAB;
gSpriteCallStub[13] = 0x6B;
gDrawStubInited = true;
}
// destOffset always changes (sprite moves every frame).
gSpriteCallStub[ 6] = (unsigned char)(destOffset & 0xFFu);
gSpriteCallStub[ 7] = (unsigned char)((destOffset >> 8) & 0xFFu);
// destBank only changes if the dst surface migrates banks (~never).
if (destBank != gDrawStubLastBank) {
gSpriteCallStub[ 2] = destBank;
gDrawStubLastBank = destBank;
}
// fnAddr changes only on shift parity flips or sprite swaps.
if (fnAddr != gDrawStubLastFnAddr) {
const uint8_t *fnB_ = (const uint8_t *)&fnAddr;
gSpriteCallStub[ 9] = fnB_[0];
gSpriteCallStub[10] = fnB_[1];
gSpriteCallStub[11] = fnB_[2];
gDrawStubLastFnAddr = fnAddr;
}
// This function is compiled under `longa on / longi on`
// (M=16, X=16) and the function epilogue assumes those
// widths at exit -- the deallocation ADC takes a 2-byte immediate
// and any LDX/LDY use 2-byte immediates. The byte writes to
// gSpriteCallStub above leave M=8, and an earlier PHP/PLP-only
// wrapper let the asm block exit in the wrong M state. The
// epilogue's `ADC #imm; TCS` then decoded as a wider ADC that
// swallowed the TCS, S was never adjusted, RTL popped wrong
// bytes, control fell into BSS, and the IIgs hit BRK on a zero
// byte. Force M=16/X=16 before returning to compiled C.
__asm__ volatile (
"rep #0x30\n\t"
"sep #0x20\n\t"
"jsl gSpriteCallStub\n\t"
"rep #0x30"
::: "memory"
);
drawFn = (void (*)(uint8_t *))fnAddr;
drawFn(dstRow0);
}
// Save/Restore call stub. The compiled MVN-row routines need
// M=16 / X=16 and expect index registers preset to source/dest
// offsets within their respective banks. MVN's own bank operands
// are in the routine bytes (patched per call), so this stub doesn't
// need to load DBR -- it just sets X and Y, JSLs, and restores DBR
// (MVN itself sets DBR to its destination bank as a side effect).
//
// Stub layout (13 bytes):
// 00: 8B PHB ; save caller DBR
// 01: A2 lo hi LDX #srcOffset
// 04: A0 lo hi LDY #dstOffset
// 07: 22 lo mid bk JSL routine
// 0B: AB PLB ; restore caller DBR
// 0C: 6B RTL
//
// For SAVE: X = screen lo, Y = backup lo
// For RESTORE: X = backup lo, Y = screen lo
//
// Two distinct stubs (one per op) instead of a shared one. Save and
// restore alternate every frame and they swap the X/Y meanings, so a
// shared stub forced a full re-stamp on every call. Per-op stubs let
// us cache: only the bytes that genuinely change frame-to-frame
// (typically just one of screenLo/backupLo as the sprite moves) get
// rewritten. Cuts per-call patching from 13 stores to 2 in the typical
// case (static backup buffer, stable shift parity).
static unsigned char gSpriteSaveStub[13];
static unsigned char gSpriteRestoreStub[13];
static bool gSaveStubInited = false;
static uint16_t gSaveStubLastXLo = 0xFFFFu;
static uint16_t gSaveStubLastYLo = 0xFFFFu;
static uint32_t gSaveStubLastFnAddr = 0xFFFFFFFFul;
static bool gRestoreStubInited = false;
static uint16_t gRestoreStubLastXLo = 0xFFFFu;
static uint16_t gRestoreStubLastYLo = 0xFFFFu;
static uint32_t gRestoreStubLastFnAddr= 0xFFFFFFFFul;
// patchMvnBanks stamps the destination and source bank operand bytes
// into each MVN inside an emitted save/restore routine. Layout from
// spriteEmitIigs.c::emitMvnCopyRoutine:
// row 0 (6 bytes): A9 lo hi 54 db sb
// row R (12 bytes, R>=1): 8A/98 18 69 lo hi AA/A8 A9 lo hi 54 db sb
// end (1 byte): 6B
// MVN dstbk is at offset (12*R + 4); srcbk at (12*R + 5).
// patchMvnBanks stamps the dst/src bank operand bytes into each MVN inside an
// emitted C-ABI save/restore routine -- the routine's only runtime-dynamic
// bytes, since MVN's banks are immediate operands and the screen/backup banks
// vary at runtime. The patch is a plain C pointer write into the arena from
// the dispatcher (no self-modifying call stub). C-ABI routine layout from
// spriteEmitIigs.c::emitMvnCabiRoutine:
// prologue (4): 8B DA AA 7A
// row 0 (6): A9 lo hi 54 db sb
// row R (12): 8A/98 18 69 lo hi AA/A8 A9 lo hi 54 db sb
// epilogue (2): AB 6B
// The 4-byte prologue shifts MVN dstbk to routine offset (12*R + 8), srcbk to
// (12*R + 9).
static void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank, uint8_t srcBank) {
uint16_t r;
for (r = 0; r < heightPx; r++) {
routine[12u * r + 4u] = dstBank;
routine[12u * r + 5u] = srcBank;
routine[12u * r + 8u] = dstBank;
routine[12u * r + 9u] = srcBank;
}
}
@ -515,53 +430,23 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
+ sp->slot->offset
+ (uint32_t)routineOffset;
// Stub: X = screen (source), Y = backup (destination).
if (!gSaveStubInited) {
gSpriteSaveStub[ 0] = 0x8B;
gSpriteSaveStub[ 1] = 0xA2;
gSpriteSaveStub[ 4] = 0xA0;
gSpriteSaveStub[ 7] = 0x22;
gSpriteSaveStub[11] = 0xAB;
gSpriteSaveStub[12] = 0x6B;
gSaveStubInited = true;
}
if (screenLo != gSaveStubLastXLo) {
gSpriteSaveStub[ 2] = (unsigned char)(screenLo & 0xFFu);
gSpriteSaveStub[ 3] = (unsigned char)((screenLo >> 8) & 0xFFu);
gSaveStubLastXLo = screenLo;
}
if (backupLo != gSaveStubLastYLo) {
gSpriteSaveStub[ 5] = (unsigned char)(backupLo & 0xFFu);
gSpriteSaveStub[ 6] = (unsigned char)((backupLo >> 8) & 0xFFu);
gSaveStubLastYLo = backupLo;
}
if (fnAddr != gSaveStubLastFnAddr) {
/* Byte-alias the uint32_t to grab the 3 bank/lo/hi bytes
* without invoking ~LSHR4 for the >>16. */
const uint8_t *fnB_ = (const uint8_t *)&fnAddr;
gSpriteSaveStub[ 8] = fnB_[0];
gSpriteSaveStub[ 9] = fnB_[1];
gSpriteSaveStub[10] = fnB_[2];
gSaveStubLastFnAddr = fnAddr;
}
// Skip the 16+ MVN-bank rewrites if the dst/src bank pair is the
// same as last call.
// Patch the MVN bank operands only if the dst/src bank pair changed since
// last call (the routine's only dynamic bytes). SAVE copies screen ->
// backup: dst = backup bank, src = screen bank.
if (*cachedDst != backupBank || *cachedSrc != screenBank) {
routine = codegenArenaBase() + sp->slot->offset + routineOffset;
routine = (uint8_t *)fnAddr;
patchMvnBanks(routine, heightPx, /*dst*/backupBank, /*src*/screenBank);
*cachedDst = backupBank;
*cachedSrc = screenBank;
}
// MVN-based routine: needs M=16 / X=16; restore M=16 on exit
// matches the `longa on` epilogue expectations.
__asm__ volatile (
"rep #0x30\n\t"
"jsl gSpriteSaveStub\n\t"
"rep #0x30"
::: "memory"
);
// Clean C-ABI dispatch -- void save(uint32_t packed), packed = srcOff |
// (dstOff << 16). The compiler emits the indirect JSL: no self-modifying
// call stub, no inline asm. SAVE: src = screen offset, dst = backup offset.
{
uint32_t packed = (uint32_t)screenLo | ((uint32_t)backupLo << 16);
((void (*)(uint32_t))fnAddr)(packed);
}
}
@ -600,51 +485,25 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
+ sp->slot->offset
+ (uint32_t)routineOffset;
// Stub: X = backup (source), Y = screen (destination).
if (!gRestoreStubInited) {
gSpriteRestoreStub[ 0] = 0x8B;
gSpriteRestoreStub[ 1] = 0xA2;
gSpriteRestoreStub[ 4] = 0xA0;
gSpriteRestoreStub[ 7] = 0x22;
gSpriteRestoreStub[11] = 0xAB;
gSpriteRestoreStub[12] = 0x6B;
gRestoreStubInited = true;
}
if (backupLo != gRestoreStubLastXLo) {
gSpriteRestoreStub[ 2] = (unsigned char)(backupLo & 0xFFu);
gSpriteRestoreStub[ 3] = (unsigned char)((backupLo >> 8) & 0xFFu);
gRestoreStubLastXLo = backupLo;
}
if (screenLo != gRestoreStubLastYLo) {
gSpriteRestoreStub[ 5] = (unsigned char)(screenLo & 0xFFu);
gSpriteRestoreStub[ 6] = (unsigned char)((screenLo >> 8) & 0xFFu);
gRestoreStubLastYLo = screenLo;
}
if (fnAddr != gRestoreStubLastFnAddr) {
const uint8_t *fnB_ = (const uint8_t *)&fnAddr;
gSpriteRestoreStub[ 8] = fnB_[0];
gSpriteRestoreStub[ 9] = fnB_[1];
gSpriteRestoreStub[10] = fnB_[2];
gRestoreStubLastFnAddr = fnAddr;
}
// Same short-circuit as save: only re-stamp the bank operands if
// they actually changed since last call.
// Patch the MVN bank operands only if they changed (the routine's only
// dynamic bytes). RESTORE copies backup -> screen: dst = screen bank,
// src = backup bank.
if (*cachedDst != screenBank || *cachedSrc != backupBank) {
routine = codegenArenaBase() + sp->slot->offset + routineOffset;
routine = (uint8_t *)fnAddr;
patchMvnBanks(routine, heightPx, /*dst*/screenBank, /*src*/backupBank);
*cachedDst = screenBank;
*cachedSrc = backupBank;
}
__asm__ volatile (
"rep #0x30\n\t"
"jsl gSpriteRestoreStub\n\t"
"rep #0x30"
::: "memory"
);
// Clean C-ABI dispatch -- void restore(uint32_t packed), packed = srcOff |
// (dstOff << 16). RESTORE: src = backup offset, dst = screen offset.
{
uint32_t packed = (uint32_t)backupLo | ((uint32_t)screenLo << 16);
((void (*)(uint32_t))fnAddr)(packed);
}
}
#elif defined(JOEYLIB_PLATFORM_AMIGA)
/* Amiga planar dispatchers. spriteEmitPlanar68k.c emits routines with

View file

@ -42,7 +42,7 @@
// ----- Prototypes -----
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
static uint32_t emitMvnCabiRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
@ -151,30 +151,48 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
// later row = 12. Plus the trailing RTL.
#define IIGS_MVN_MAX_BYTES_PER_ROW 12u
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
// C-ABI MVN copy routine. Called via a normal C function pointer:
// void copy(uint32_t packed); packed = srcOff | (dstOff << 16)
// so arg0 arrives as A = srcOff, X = dstOff (llvm-mos w65816 cdecl). The
// routine reaches MVN's required state (M=16, X=16 -- the C ABI entry state --
// with X = src offset, Y = dst offset) via a tiny fixed prologue, runs the
// unrolled MVN rows, then restores the caller's DBR (MVN leaves DBR = dst bank)
// and returns. No self-modifying call stub, no inline asm.
//
// The MVN bank operand bytes are still patched per call (MVN's banks are
// immediate operands and the screen/backup banks are runtime-dynamic), but the
// patch is a plain C pointer write into the arena from the dispatcher -- the
// dst bank for row R is at routine offset (12*R + 8), src bank at (12*R + 9)
// (the 4-byte prologue shifts the old +4/+5 offsets by 4).
//
// Layout: prologue (4): 8B DA AA 7A (PHB; PHX; TAX; PLY)
// row 0 (6): A9 lo hi 54 db sb
// row R (12): 8A/98 18 69 lo hi AA/A8 A9 lo hi 54 db sb
// epilogue(2): AB 6B (PLB; RTL)
static uint32_t emitMvnCabiRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
uint32_t cursor;
uint16_t advance;
uint16_t row;
// MVN copies (count + 1) bytes from the LDA #(copyBytes-1) operand.
// A zero copyBytes would emit LDA #0xFFFF and MVN a full 64 KB --
// a screen-trashing blit. widthTiles==0 is rejected at sprite
// creation, so copyBytes>=4 in practice, but guard the helper so a
// future caller cannot weaponize it: emit a bare RTL no-op instead.
// A zero copyBytes would emit LDA #0xFFFF and MVN a full 64 KB -- guard it.
if (copyBytes == 0u) {
if (cap < 1u) {
return SPRITE_EMIT_OVERFLOW;
}
out[0] = 0x6B;
return 1u;
return 0u;
}
cursor = 0;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
// C-ABI prologue: A = srcOff, X = dstOff -> X = srcOff, Y = dstOff.
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x8B; // PHB ; save caller DBR
out[cursor++] = 0xDA; // PHX ; push dstOff
out[cursor++] = 0xAA; // TAX ; X = A = srcOff
out[cursor++] = 0x7A; // PLY ; Y = dstOff
for (row = 0; row < heightPx; row++) {
// Reserve the row's worst case plus the trailing RTL.
if (cursor + IIGS_MVN_MAX_BYTES_PER_ROW + 1u > cap) {
if (cursor + IIGS_MVN_MAX_BYTES_PER_ROW + 2u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
if (row > 0) {
@ -192,9 +210,12 @@ static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx
out[cursor++] = 0x00; // dstbk -- patched per call
out[cursor++] = 0x00; // srcbk -- patched per call
}
if (cursor + 1u > cap) {
// C-ABI epilogue: restore caller DBR (MVN left it = dst bank), return.
if (cursor + 2u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0xAB; // PLB
out[cursor++] = 0x6B; // RTL
return cursor;
}
@ -218,7 +239,9 @@ uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/true);
// SAVE: src = screen (X advances SURFACE_BYTES_PER_ROW per row), dst =
// backup (Y, contiguous). The dispatcher packs srcOff=screen, dstOff=backup.
return emitMvnCabiRoutine(out, cap, heightPx, copyBytes, /*advanceX=*/true);
}
@ -238,7 +261,10 @@ uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp,
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/false);
// RESTORE: src = backup (X, contiguous), dst = screen (Y advances
// SURFACE_BYTES_PER_ROW per row). The dispatcher packs srcOff=backup,
// dstOff=screen.
return emitMvnCabiRoutine(out, cap, heightPx, copyBytes, /*advanceX=*/false);
}
@ -297,8 +323,23 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
wide = false;
// No prologue: caller (the inline-asm stub in jlSpriteCompile.c)
// sets M=8/X=16/Y=destRow before JSL'ing here.
// C-ABI prologue. The runtime calls this routine via a normal C
// function pointer -- void draw(uint8_t *dstRow0) -- so the compiler
// emits the JSL and there is no self-modifying stub. The 24-bit
// dstRow0 pointer arrives in A = low 16, X = bank (llvm-mos w65816
// cdecl). Establish the draw body's expected entry state:
// M = 8, X = 16, Y = dstRow0 offset, DBR = dstRow0 bank
// and preserve the caller's DBR.
if (cursor + 7u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x8B; // PHB ; save caller DBR
out[cursor++] = 0xA8; // TAY ; Y = A = dstRow0 offset (M=16)
out[cursor++] = 0xE2; // SEP #$20 ; M=8
out[cursor++] = 0x20;
out[cursor++] = 0x8A; // TXA ; A = X low byte = dstRow0 bank
out[cursor++] = 0x48; // PHA
out[cursor++] = 0xAB; // PLB ; DBR = dstRow0 bank
for (row = 0; row < heightPx; row++) {
for (col = 0; col < destBytesPerRow; col++) {
@ -374,26 +415,16 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
}
}
// Trailing SEP (2 bytes if still wide) + RTL (1 byte). The per-byte
// reserve above accounts for these whenever a byte was emitted, but
// an all-transparent sprite never entered the inner write, so guard
// the epilogue explicitly.
if (cursor + (wide ? 2u : 0u) + 1u > cap) {
// C-ABI epilogue: normalize to M=16 (REP clears M whether the body left
// it 8 or 16 after a word run), restore the caller's DBR, return. X was
// never touched by the body, so it is still 16-bit for the caller.
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// Routine exits in M=8: the JSL stub assumes M=8 throughout (the
// stub itself only ever ran with M=8 and doesn't restore M). The
// asm wrapper after the JSL forces M=16 again, but be defensive
// and ensure we leave M=8 here so the stub's PLB/RTL run as
// expected even if the wrapper convention changes.
if (wide) {
out[cursor++] = 0xE2;
out[cursor++] = 0xC2; // REP #$20 ; M=16
out[cursor++] = 0x20;
}
// Epilogue: rtl (large memory model -b uses JSL/RTL).
out[cursor++] = 0x6B;
out[cursor++] = 0xAB; // PLB ; restore caller DBR
out[cursor++] = 0x6B; // RTL
return cursor;
}

View file

@ -27,6 +27,17 @@ bool halInit(const jlConfigT *config);
// Per-port teardown. Restores display mode, frees HW-adjacent buffers.
void halShutdown(void);
// Large / persistent allocation backing for the public jlAlloc / jlFree.
// Ports with a tiny or per-allocation-capped C heap (IIgs: bank-0 heap,
// ~32 KB malloc cap) implement these with their native allocator (IIgs:
// Memory Manager NewHandle / DisposeHandle). Ports with a normal heap
// use the shared malloc / free default defined in core/init.c, so only
// the constrained ports need a per-port override. halBigAlloc returns
// NULL on failure; the returned memory is not zero-filled. halBigFree
// accepts NULL.
void *halBigAlloc(uint32_t bytes);
void halBigFree(void *p);
// 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

View file

@ -5,6 +5,7 @@
// jlShutdown tears those down in reverse order.
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "joey/core.h"
@ -44,8 +45,40 @@ void coreSetError(const char *message) {
}
// ----- Default large-allocation backing -----
//
// jlAlloc / jlFree route through halBigAlloc / halBigFree. Most ports
// have a normal heap, so the default below (plain malloc / free) serves
// them and they do NOT need to implement the hook themselves. Only ports
// whose C heap is too small or per-allocation-capped to satisfy a large
// request -- currently just the IIgs -- provide their own halBigAlloc /
// halBigFree (src/port/iigs/hal.c) and compile out this default.
#ifndef JOEYLIB_PLATFORM_IIGS
void *halBigAlloc(uint32_t bytes) {
return malloc((size_t)bytes);
}
void halBigFree(void *p) {
free(p);
}
#endif
// ----- Public API (alphabetical) -----
void *jlAlloc(uint32_t bytes) {
return halBigAlloc(bytes);
}
void jlFree(void *p) {
halBigFree(p);
}
bool jlInit(const jlConfigT *config) {
clearError();

View file

@ -340,7 +340,9 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
// Fast path: compiled bytes + fully on surface. Off-surface draws
// fall back to the interpreter so the compiled routines never
// need clip math (they walk fixed offsets).
// need clip math (they walk fixed offsets). On IIgs sp->slot is left
// NULL by jlSpriteCompile (see #19 there), so this is never taken and
// every op runs the interpreter.
if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
spriteCompiledDraw(s, sp, x, y);
if (!COMPILED_SPRITE_WRITES_PLANES) {

View file

@ -4,13 +4,13 @@
// code from non-AUDIO binaries, which pay only for the data
// references they actually use.
//
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
// iigs.mk xxd-i header rule. halAudioInit BlockMoves those bytes into
// a fixed-bank Memory Manager handle and JSLs into them through the
// self-modifying call stub below; NTP is bank-internal / position-
// independent so it runs at whatever address Memory Manager picked.
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time to
// ntpplayer.bin and LOADED FROM DISK at runtime (NTP_PATH). halAudioInit
// reads those bytes into a fixed-bank Memory Manager handle and JSLs into
// them through the self-modifying call stub below; NTP is bank-internal /
// position-independent so it runs at whatever address Memory Manager picked.
#include <stdio.h>
#include <string.h>
// Memory Manager via <iigs/toolbox.h>. Handles/pointers are void* and the
@ -31,12 +31,13 @@ typedef void *Pointer;
#include "hal.h"
#include "joey/audio.h"
// The 34 KB NTP replayer bytes live in their own data segment
// (build/iigs/audio/ntpdata.asm, auto-generated from ntpplayer.bin by
// make/iigs.mk). We just extern the symbols here.
extern const unsigned char gNtpPlayerBytes[];
extern const unsigned long gNtpPlayerBytes_len;
// The ~34 KB NTP replayer is LOADED FROM DISK at runtime, not baked into
// this TU. As rodata it overflows the IIgs bank-0 ceiling, and clang's near
// (IMM16 + $00BE load-bank) form for &blob cannot reach a far rodata segment
// in another bank -- so a baked blob is impossible on the IIgs regardless of
// link816 splitting. ntpplayer.bin must ship alongside the application and is
// resolved against the current GS/OS prefix.
#define NTP_PATH "ntpplayer"
// ----- Constants -----
@ -142,7 +143,10 @@ static uint16_t gSfxSlotRateHz[JOEY_AUDIO_SFX_SLOTS] = { 0 };
// 08: A9 lo hi LDA #A
// 0B: 22 lo hi bk JSL target
// 0F: 6B RTL
static unsigned char gCallStub[16];
// GLOBAL (not static): referenced by name from the inline `jsl gCallStub`
// blocks below; link816 resolves .text relocations only against global
// symbols, so a file-local buffer links as "unresolved gCallStub".
unsigned char gCallStub[16];
// ----- Internal helpers -----
@ -169,6 +173,8 @@ static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) {
static bool loadNTP(void) {
Handle h;
Pointer p;
FILE *fp;
size_t got;
h = NewHandle(NTP_BUFFER_BYTES, _ownerid,
attrFixed | attrLocked | attrPage | attrNoCross,
@ -183,7 +189,19 @@ static bool loadNTP(void) {
return false;
}
BlockMove((Pointer)gNtpPlayerBytes, p, gNtpPlayerBytes_len);
// Read the replayer from disk (NTP_PATH) into the locked, bank-internal
// handle -- see the NTP_PATH note above for why it is not baked in.
fp = fopen(NTP_PATH, "rb");
if (fp == NULL) {
DisposeHandle(h);
return false;
}
got = fread(p, 1, NTP_BUFFER_BYTES, fp);
fclose(fp);
if (got == 0) {
DisposeHandle(h);
return false;
}
gNTPHandle = h;
gNTPBase = (uint32_t)p;
@ -417,36 +435,89 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
}
// IIgs has the Ensoniq DOC + a $C030 toggle for the system beep.
// A tone-mode driver could program a DOC channel with a perfect
// square wave OR pulse $C030 in a timer ISR. Not wired yet --
// AGI-on-IIgs will be silent until this is filled in.
void halAudioTone(uint16_t freqHz) {
(void)freqHz;
// PSG-style tone/voice synthesis. A PSG square-wave voice is just a
// synthesized square sample (re)triggered on one of the SFX slots, which
// already cooperate with the NTP MOD engine (SFX uses DOC oscillators
// 14-21; NTP uses the lower ones). Loudness comes from the sample
// amplitude, so no per-oscillator DOC volume plumbing is needed; the
// host re-asserts active voices each frame via halAudioFrameTick (the
// registered tick fn), which sustains them between the one-shot triggers.
#define TONE_RATE 26000U // DOC playback rate for synthesized PSG
#define TONE_BUF_BYTES 2048 // ~2 frames of square wave @ TONE_RATE
#define AGI_ATTEN_OFF 15U // AGI attenuation: 0 = loudest .. 15 = silent
#define IIGS_FRAME_HZ 60U
static int8_t gToneBuf[TONE_BUF_BYTES];
static jlAudioTickFnT gTickFn = (jlAudioTickFnT)0;
static uint16_t gTickReload = 1;
static uint16_t gTickCount = 1;
// Fill gToneBuf with whole periods of a signed-8-bit square wave at the
// given half-period (samples per half-cycle) and amplitude; returns the
// byte length written (a whole number of periods so re-triggers seam).
static uint16_t toneFill(uint16_t halfPeriod, int8_t amp) {
uint16_t period;
uint16_t len;
uint16_t i;
if (halfPeriod == 0) {
halfPeriod = 1;
}
period = (uint16_t)(halfPeriod * 2u);
len = (uint16_t)((TONE_BUF_BYTES / period) * period);
if (len == 0) {
len = (period <= TONE_BUF_BYTES) ? period : TONE_BUF_BYTES;
}
for (i = 0; i < len; i++) {
gToneBuf[i] = ((uint16_t)(i % period) < halfPeriod) ? amp : (int8_t)(-(int)amp);
}
return len;
}
// 3-voice PSG-style tone: full IIgs implementation would dedicate
// three DOC oscillators to looping a tiny square waveform from DOC
// RAM, programming each channel's frequency divisor + volume. The
// existing NTPstreamsound NTP engine uses all 32 oscillators for
// MOD playback, so a voice-mode driver needs its own oscillator
// allocation that backs off when NTP is playing. Stubbed for now;
// AGI-on-IIgs is silent until this lands.
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
(void)voice;
(void)freqHz;
(void)atten;
uint16_t halfPeriod;
uint16_t len;
int8_t amp;
if (voice >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
if (freqHz == 0u || atten >= AGI_ATTEN_OFF) {
halAudioStopSfx(voice); // freq 0 / max atten = silence
return;
}
// AGI attenuation 0..14 -> amplitude 127..8 (linear; loudness is in the
// sample, the DOC plays the slot at full volume).
amp = (int8_t)(127 - ((uint16_t)atten * 119u) / (AGI_ATTEN_OFF - 1u));
halfPeriod = (uint16_t)((TONE_RATE / 2u) / freqHz);
len = toneFill(halfPeriod, amp);
// gToneBuf is shared across voices and its contents change every call,
// so bust halAudioPlaySfx's (ptr,len,rate) copy-cache to force a refresh.
gSfxSlotSample[voice] = (const uint8_t *)0;
halAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE);
}
void halAudioTone(uint16_t freqHz) {
halAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0
}
// Frame-synced tick. The IIgs VBL is ~60 Hz and AGI sound is 60 Hz, so we
// ride the host's per-frame jlAudioFrameTick pump rather than install a
// raw VBL ISR (which would fight GS/OS heartbeat tasks). hz > 60 clamps to
// per-frame; lower rates divide down.
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
// IIgs VBL interrupt at $C019 would be the natural hook, but
// it's capped to ~60 Hz and AGI sound is already 60 Hz, so the
// wiring is a no-op when the voice driver above goes silent.
(void)fn;
(void)hz;
if (fn == (jlAudioTickFnT)0 || hz == 0u) {
gTickFn = (jlAudioTickFnT)0;
return false;
}
gTickFn = fn;
gTickReload = (hz >= IIGS_FRAME_HZ) ? 1u : (uint16_t)(IIGS_FRAME_HZ / hz);
gTickCount = gTickReload;
return true;
}
@ -496,5 +567,14 @@ void halAudioStopSfx(uint8_t slot) {
void halAudioFrameTick(void) {
// NTP is DOC-IRQ driven; nothing for the host to pump.
// NTP is DOC-IRQ driven; nothing to pump there. But a registered audio
// tick (halAudioTickRegister) rides this per-frame hook to re-assert PSG
// voices and advance AGI-style sound at up to IIGS_FRAME_HZ.
if (gTickFn != (jlAudioTickFnT)0) {
gTickCount--;
if (gTickCount == 0u) {
gTickCount = gTickReload;
gTickFn();
}
}
}

View file

@ -32,6 +32,20 @@
#include "hal.h"
#include "surfaceInternal.h"
// Memory Manager surface for halBigAlloc / halBigFree (jlAlloc backing).
// Same idiom as src/port/iigs/audio_full.c: Handles/pointers are void*,
// the owner ID comes from MMStartUp(), and the attribute bit values are
// Apple's Memory Manager constants. Only the bits we use are defined.
#include <iigs/toolbox.h>
typedef void *Handle;
#define _ownerid (MMStartUp())
#define attrFixed 0x4000
#define attrLocked 0x8000
// Leading bookkeeping slot: jlAlloc/jlFree traffic in plain pointers but
// DisposeHandle needs the Handle, so we over-allocate by one Handle and
// stash it just before the pointer we return.
#define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle))
/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick
* ($2503) and returns the low 16 bits of the system's tick counter
* (firmware VBL ISR-driven). Polling $C019 from C user code missed
@ -82,7 +96,7 @@ extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1
// The asm performs the upload directly rather than a C-side memcpy,
// which is unreliable when called from halPresent (DBR-state quirk
// after prior asm primitives).
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by halFastFloodWalkAndScans.
extern uint16_t gFloodSeedMatch;
@ -204,6 +218,46 @@ static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) {
// ----- HAL API (alphabetical) -----
// Large/persistent allocation via the Memory Manager. The IIgs C heap
// is bank-0-only and a single malloc is capped near 32 KB, so buffers
// like the AGI view cache (~1.3 MB) and the PIC visual/priority buffers
// must be NewHandle'd. We request attrFixed|attrLocked so the block
// neither moves nor purges while the caller holds the dereferenced
// pointer. The 65816 has no alignment fault, so the BIG_ALLOC_HDR-byte
// offset applied to the master pointer is safe for any access pattern.
// The returned memory is not zero-filled (matches malloc semantics).
void *halBigAlloc(uint32_t bytes) {
Handle h;
uint8_t *base;
h = NewHandle((unsigned long)(bytes + BIG_ALLOC_HDR), _ownerid,
(unsigned short)(attrFixed | attrLocked), NULL);
if (h == NULL) {
return NULL;
}
HLock(h);
base = *(uint8_t **)h;
if (base == NULL) {
DisposeHandle(h);
return NULL;
}
// Stash the Handle in the leading slot so halBigFree can recover it.
*(Handle *)base = h;
return base + BIG_ALLOC_HDR;
}
void halBigFree(void *p) {
Handle h;
if (p == NULL) {
return;
}
h = *(Handle *)((uint8_t *)p - BIG_ALLOC_HDR);
DisposeHandle(h);
}
bool halInit(const jlConfigT *config) {
(void)config;
gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
@ -232,22 +286,23 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) {
uint16_t uploadFlags;
if (src == NULL) {
return;
}
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
// C-side memcpy to bank $E1 has been unreliable from
// halPresent's calling context, so we route everything through
// the asm path. uploadFlags gates the optional SCB / palette
// uploads on the dirty bits so we don't re-push 712 unchanged
// bytes every frame: bit 0 = SCB, bit 1 = palette (pixels always).
uploadFlags = (uint16_t)gStageScbDirty | ((uint16_t)gStagePaletteDirty << 1);
iigsBlitStageToShr(src->scb, &src->palette[0][0], uploadFlags);
gStagePaletteDirty = false;
gStageScbDirty = false;
// SCB + palette upload via the C-side memcpy to bank $E1, then pixels.
// The asm palette MVN in iigsBlitStageToShr never wrote $E1:9E00 -- the
// SCB MVN landed but colors showed the GS/OS default (pixels/shapes were
// correct, every color wrong). The C memcpy is reliable now that the
// far-const-pointer bank-drop miscompile is fixed (it formerly hit bank
// $00). uploadScbAndPaletteIfNeeded clears the dirty bits, so the asm
// blit (uploadFlags == 0) does pixels only. The asm's now-unreached
// SCB/palette MVN code is dead -- a removal candidate.
// KNOWN RESIDUAL: on a DRAW the PEI-slam corrupts $E1:9E20+ (palettes
// 1-15; palette 0 and pixels stay correct) -- a peislam.s shadow / soft-
// switch spill, independent of upload order. Single-palette apps are
// unaffected; multi-palette colors above index 0 need the slam fix.
uploadScbAndPaletteIfNeeded(src);
iigsBlitStageToShr(src->scb, &src->palette[0][0], 0);
}

View file

@ -265,17 +265,19 @@ void halJoystickReset(jlJoystickE js) {
gJoyRecalibrate[js] = true;
}
// Asm paddle reader (joeyDraw.asm). Switches CPU to 1 MHz for the
// duration of the poll so paddle counts match what every other
// IIgs/Apple II joystick game produces (the C busy-wait at 2.8 MHz
// inflated counts). Returns results via gJoy* DRAWPRIMS scratch.
extern void iigsPollJoystickInner(void);
extern volatile uint8_t gJoyPx;
extern volatile uint8_t gJoyPy;
extern volatile uint8_t gJoyResolved; // bit0: pdl0 fired; bit1: pdl1 fired
// Asm paddle reader (joeyDraw.s, John Brooks' 1 MHz GetJoyXY). Switches
// the CPU to 1 MHz for the read so paddle counts match what every other
// IIgs/Apple II joystick game produces (a busy-wait at 2.8 MHz inflates
// counts). Returns the paddle read packed as a uint32_t in A:X (register
// return -- asm writes to a global do not reach the C-read address in
// this toolchain):
// resolved = ret & 0xFF (bit0: JoyX valid, bit1: JoyY valid)
// px = (ret >> 8) & 0xFF (JoyX 0..255)
// py = (ret >> 16) & 0xFF (JoyY 0..255)
extern uint32_t iigsPollJoystickInner(void);
static void pollJoystick(void) {
uint32_t result;
uint8_t px;
uint8_t py;
uint8_t resolvedFlags;
@ -301,11 +303,11 @@ static void pollJoystick(void) {
return;
}
// Asm read at 1 MHz -- accurate paddle counts.
iigsPollJoystickInner();
px = gJoyPx;
py = gJoyPy;
resolvedFlags = gJoyResolved;
// Asm read at 1 MHz -- result returned packed in registers (A:X).
result = iigsPollJoystickInner();
resolvedFlags = (uint8_t)(result & 0xFFu);
px = (uint8_t)((result >> 8) & 0xFFu);
py = (uint8_t)((result >> 16) & 0xFFu);
xResolved = (resolvedFlags & 0x01) != 0;
yResolved = (resolvedFlags & 0x02) != 0;

View file

@ -142,8 +142,11 @@ clrExit:
; inhibited by halInit. ~1.5x faster.
;
; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI.
; SAFE ONLY POST-halInit. Soft switches are bank-0 16-bit absolute
; (DBR = 0). `pixels` (arg0) in A:X is ignored; fillWord (arg1) at 5,s.
; SAFE ONLY POST-halInit. Soft switches use LONG addressing to bank $E0
; ($E0C005), NOT `sta $C005`: under the GS/OS Loader DBR=$08, a DBR-rel
; `sta $C0xx` hits bank-$08 RAM and the switch silently never toggles, so
; AUXWRITE stays off and the slam corrupts bank 0 instead of the $01
; stage. `pixels` (arg0) in A:X is ignored; fillWord (arg1) at 5,s.
; ----------------------------------------------------------------
.section .text.iigsSurfaceClearFastInner,"ax"
.globl iigsSurfaceClearFastInner
@ -159,7 +162,7 @@ iigsSurfaceClearFastInner:
sta sclrSavedSp ; save original SP
sep #0x20
.a8
sta 0xc005 ; AUXWRITE on: $00:0200-BFFF writes -> bank $01
sta 0xE0C005 ; AUXWRITE on: $00:0200-BFFF writes -> bank $01
rep #0x20
.a16
lda #0x9cff
@ -253,7 +256,7 @@ clrFastBlock:
tcs ; restore original SP
sep #0x20
.a8
sta 0xc004 ; AUXWRITE off
sta 0xE0C004 ; AUXWRITE off
rep #0x20
.a16
.a8
@ -312,14 +315,17 @@ gFloodRightX:
; --------------------------------------------------------------------
; Bank-0 scratch pointer block used by the two-pointer tile routines
; (Copy / Paste / Snap / CopyMasked). The clang ABI hands us dst in
; A:X and src on the stack, but the original copy loops assume a
; D-frame with dst at [D+0] and src at [D+4]. We rebuild that frame in
; this bank-0 scratch, point D at it, and run the original [0],y /
; [4],y indirect-long loads unchanged.
; tileScratchPtrs+0..2 = dst (low16 + bank) <- [0],y
; tileScratchPtrs+4..6 = src (low16 + bank) <- [4],y
; STAGING pointer block for the two-pointer tile routines (Copy / Paste
; / Snap / CopyMasked). The clang ABI hands us dst in A:X and src on the
; stack, but the copy loops assume a D-frame with dst at [D+0] and src
; at [D+4]. We assemble the 8 bytes here first, then COPY them onto the
; hardware stack and point D there -- D (the direct page) is ALWAYS bank
; 0, but this BSS block lives in the LOAD bank, so D must NOT point at it
; directly (that was the historical bug: [0],y/[4],y then read bank 0
; while the stores wrote the load bank). The stack is bank 0, so the
; copied frame matches.
; tileScratchPtrs+0..2 = dst (low16 + bank) (staged, then -> [0],y)
; tileScratchPtrs+4..6 = src (low16 + bank) (staged, then -> [4],y)
; (bytes +3 / +7 are unused padding, mirroring the 4-byte ptr layout)
; --------------------------------------------------------------------
.section .bss.tileScratchPtrs,"aw"
@ -362,15 +368,19 @@ iigsTileFillInner:
.a16
.i16
; Build the dst D-frame on the STACK (bank 0). A BSS scratch
; (tileScratchPtrs) lives in the LOAD bank, but D (the direct
; page) is ALWAYS bank 0, so the old `tcd #tileScratchPtrs`
; pointed [0],y at bank-0:offset while the stores wrote
; loadbank:offset -- the indirect read the wrong bank entirely.
; The hardware stack is bank 0, so a stack D-frame matches.
; A = dst low16, X = dst high16 (bank in low byte).
sta tileScratchPtrs+0 ; dst low16
stx tileScratchPtrs+2 ; dst high16 (bank + pad)
lda 8,s ; fillWord (post +4 push)
pha ; stash fillWord (+2)
lda #tileScratchPtrs
tcd ; D -> scratch; [0] = dst
pla ; A = fillWord again
; (PLA balances the PHA so the stack is clean for PLD.)
phx ; dst high16 -> 3,s
pha ; dst low16 -> 1,s
tsc
inc a ; A = SP+1
tcd ; D -> stacked dst; [0] = dst
lda 12,s ; fillWord (8,s + 4 just pushed)
ldy #0
sta [0], y
@ -410,6 +420,10 @@ iigsTileFillInner:
rep #0x30
.a16
.i16
tsc
clc
adc #4 ; drop the dst D-frame (phx+pha)
tcs
pld ; restore caller D
plb
plp
@ -447,8 +461,21 @@ iigsTileCopyInner:
sta tileScratchPtrs+4
lda 10,s ; src bank
sta tileScratchPtrs+6
lda #tileScratchPtrs
tcd ; D -> scratch; [0]=dst [4]=src
; Copy the staged dst/src into a STACK (bank-0) D-frame: the
; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the
; direct page) is ALWAYS bank 0, so [0],y/[4],y via
; `tcd #tileScratchPtrs` indirected through the wrong bank.
lda tileScratchPtrs+6
pha ; src bank -> 7,s
lda tileScratchPtrs+4
pha ; src low16 -> 5,s
lda tileScratchPtrs+2
pha ; dst high16 -> 3,s
lda tileScratchPtrs+0
pha ; dst low16 -> 1,s
tsc
inc a ; A = SP+1
tcd ; D -> stacked {dst,src}; [0]=dst [4]=src
ldy #0
lda [4], y
@ -504,6 +531,10 @@ iigsTileCopyInner:
rep #0x30
.a16
.i16
tsc
clc
adc #8 ; drop the {dst,src} D-frame
tcs
pld ; restore caller D
plb
plp
@ -537,8 +568,21 @@ iigsTilePasteInner:
sta tileScratchPtrs+4
lda 10,s ; src bank
sta tileScratchPtrs+6
lda #tileScratchPtrs
tcd ; D -> scratch; [0]=dst [4]=src
; Copy the staged dst/src into a STACK (bank-0) D-frame: the
; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the
; direct page) is ALWAYS bank 0, so [0],y/[4],y via
; `tcd #tileScratchPtrs` indirected through the wrong bank.
lda tileScratchPtrs+6
pha ; src bank -> 7,s
lda tileScratchPtrs+4
pha ; src low16 -> 5,s
lda tileScratchPtrs+2
pha ; dst high16 -> 3,s
lda tileScratchPtrs+0
pha ; dst low16 -> 1,s
tsc
inc a ; A = SP+1
tcd ; D -> stacked {dst,src}; [0]=dst [4]=src
; Row 0: src word 0 = 0, dst word 0 = 0
ldy #0
@ -616,6 +660,10 @@ iigsTilePasteInner:
rep #0x30
.a16
.i16
tsc
clc
adc #8 ; drop the {dst,src} D-frame
tcs
pld ; restore caller D
plb
plp
@ -649,8 +697,21 @@ iigsTileSnapInner:
sta tileScratchPtrs+4
lda 10,s ; src bank
sta tileScratchPtrs+6
lda #tileScratchPtrs
tcd ; D -> scratch; [0]=dst [4]=src
; Copy the staged dst/src into a STACK (bank-0) D-frame: the
; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the
; direct page) is ALWAYS bank 0, so [0],y/[4],y via
; `tcd #tileScratchPtrs` indirected through the wrong bank.
lda tileScratchPtrs+6
pha ; src bank -> 7,s
lda tileScratchPtrs+4
pha ; src low16 -> 5,s
lda tileScratchPtrs+2
pha ; dst high16 -> 3,s
lda tileScratchPtrs+0
pha ; dst low16 -> 1,s
tsc
inc a ; A = SP+1
tcd ; D -> stacked {dst,src}; [0]=dst [4]=src
; Row 0: src 0/2, dst 0/2
ldy #0
@ -728,6 +789,10 @@ iigsTileSnapInner:
rep #0x30
.a16
.i16
tsc
clc
adc #8 ; drop the {dst,src} D-frame
tcs
pld ; restore caller D
plb
plp
@ -768,16 +833,29 @@ iigsTileCopyMaskedInner:
sta tileScratchPtrs+4
lda 10,s ; src bank
sta tileScratchPtrs+6
lda #tileScratchPtrs
tcd ; D -> scratch; [0]=dst [4]=src
; Copy the staged dst/src into a STACK (bank-0) D-frame: the
; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the
; direct page) is ALWAYS bank 0, so [0],y/[4],y via
; `tcd #tileScratchPtrs` indirected through the wrong bank.
lda tileScratchPtrs+6
pha ; src bank -> 7,s
lda tileScratchPtrs+4
pha ; src low16 -> 5,s
lda tileScratchPtrs+2
pha ; dst high16 -> 3,s
lda tileScratchPtrs+0
pha ; dst low16 -> 1,s
tsc
inc a ; A = SP+1
tcd ; D -> stacked {dst,src}; [0]=dst [4]=src
; Pre-compute scratch values from `transparent`.
; Original read D+8 (the stack arg); here transparent's
; low byte is at 12,s (see ABI note).
; transparent sits @12,s after the prologue; the 4-word stack
; D-frame pushed just above adds 8 more, so it is now @20,s.
sep #0x20 ; M=8
.a8
lda 12,s ; transparent (low byte)
lda 20,s ; transparent (low byte)
and #0x0F
sta tmaskTLo ; tLo = T
@ -820,6 +898,10 @@ tmaskRowLoop:
rep #0x20 ; M=16 before epilogue
.a16
.i16
tsc
clc
adc #8 ; drop the {dst,src} D-frame
tcs
pld ; restore caller D
plb
plp
@ -1330,13 +1412,6 @@ friSlamWords:
iigsDrawPixelInner:
pix = 0 ; pixels far ptr (D+0..2 = dpxlScratch)
; Stash pixels far pointer (A:X) into bank-0 scratch before any push.
rep #0x30
.a16
.i16
sta dpxlScratch ; low 16 bits of pixels
stx dpxlScratch+2 ; bank byte (+ filler)
php
phb
phd
@ -1344,9 +1419,20 @@ pix = 0 ; pixels far ptr (D+0..2 = dpxlScra
.a16
.i16
; Repoint D at the scratch struct so [pix],y (pix=0) is DP-indirect-long.
; Stash the pixels far pointer into the DP scratch via D-RELATIVE stores
; (AFTER tcd): a plain `sta dpxlScratch` writes the LOAD bank (DBR) while
; [pix],y reads through D, which is ALWAYS bank 0 -- the two disagreed and the
; plot pointer was garbage. phx/pha preserve pix (A:X survive php/phb/phd)
; across the `lda #dpxlScratch` that clobbers A; pulled back so the N,s arg
; offsets below keep their values.
phx ; save pix bank
pha ; save pix offset
lda #dpxlScratch
tcd
tcd ; D = dpxlScratch (bank 0); [pix],y == [0],y
pla ; pix offset
sta pix+0 ; D-relative -> bank0:dpxlScratch+0
pla ; pix bank
sta pix+2 ; D-relative -> bank0:dpxlScratch+2
; Compute byte offset = y*160 + (x>>1) into A. Use the LUT.
lda 10,s ; y
@ -1728,11 +1814,20 @@ iigsDrawCircleInner:
; Stash the pix far pointer (still in A:X) into bank-0 scratch and
; point D at it so [pix],y works unchanged.
sta dcPixPtr ; offset low 16 bits
txa
sta dcPixPtr+2 ; low byte = bank (high byte harmless)
; Store pix into the DP scratch via D-RELATIVE stores (AFTER tcd): a
; plain `sta dcPixPtr` writes the LOAD bank (DBR) while [pix],y reads
; through D, which is ALWAYS bank 0 -- the two disagreed, so the plot
; pointer was garbage and nothing drew. phx/pha preserve pix across the
; `lda #dcPixPtr` that clobbers A, then are pulled back so the N,s arg
; offsets below do not shift.
phx ; save pix bank
pha ; save pix offset
lda #dcPixPtr
tcd ; D = &dcPixPtr ; [pix],y == [0],y
tcd ; D = &dcPixPtr (bank 0); [pix],y == [0],y
pla ; pix offset
sta pix+0 ; D-relative -> bank0:dcPixPtr+0
pla ; pix bank
sta pix+2 ; D-relative -> bank0:dcPixPtr+2
; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4.
lda dcArgNib, s
@ -2652,9 +2747,21 @@ PEI_NARROW_MAX = 32
.a16
.i16
; Rebuild the D-frame (scbPtr/palettePtr/flags) in
; bank-0 scratch. scbPtr is in A:X; palettePtr/flags are
; on the stack (post +4 push: palettePtr@8,s flags@12,s).
; DBR must be the CALLER's data bank (= the bank our globals /
; D-frame live in). crt0 set it and a JSL preserves it across
; the cross-seg call, so at entry DBR already equals that bank.
; We must NOT force DBR := PBR (`phk`): under MULTI-SEGMENT this
; asm can land in a different bank (seg2) than the globals
; (seg1), so PBR != globals bank and phk would point DBR at the
; wrong bank -> garbage dirty-band / gPei* reads -> corrupted
; present. (Two earlier GS/OS traps this path also fixes: args
; were read back via a DP frame under DBR=$08 -> wrong bank, now
; read via the SAME absolute addressing; and each MVN leaves
; DBR=$E1, undone after every MVN -- see below.)
;
; Stash args into absolute scratch (DBR = caller/globals bank).
; scbPtr in A:X; palettePtr/flags on stack (post php+phb+phd
; +4: @8,s @12,s).
sta bsFrame+0 ; scbPtr.lo (A)
stx bsFrame+2 ; scbPtr.hi (X, bank+pad)
lda 8,s ; palettePtr.lo
@ -2663,44 +2770,92 @@ PEI_NARROW_MAX = 32
sta bsFrame+6
lda 12,s ; uploadFlags
sta bsFrame+8
lda #bsFrame
tcd ; D -> bsFrame (bscb/bpal/bflags)
; Capture the caller's DBR (= globals bank, saved by phb at
; 3,s) into a FIXED bank-$E1 byte. Each MVN below leaves
; DBR=$E1; we reload this byte (long-addressed, so it is both
; DBR- and SP-independent -- the PEI slam relocates SP) to put
; DBR back at the globals bank. $E1:9DFE is in the unused SCB
; tail ($9DC8-$9DFF), never touched by the SCB/palette/pixel
; MVNs.
sep #0x20
.a8
lda 3,s ; caller DBR (globals bank)
sta 0xE19DFE ; SP/DBR-independent stash
rep #0x20
.a16
; 1. SCB upload (200 bytes) via MVN, only when the SCB dirty bit is
; set. Source bank is runtime-patched into the MVN instruction
; (encoding: $54 dst src, so byte +2 is src).
lda bflags
lda bsFrame+8 ; uploadFlags (absolute, DBR=PBR)
and #1
beq bsSkipScb
sep #0x20
.a8
lda bscb+2
lda bsFrame+2 ; scbPtr bank byte (DBR = globals)
; Self-mod the MVN src-bank byte. The store must reach the MVN
; instruction in THIS code's bank (PBR), not DBR (globals):
; under multi-seg this asm can land in seg2 while DBR=seg1, so a
; DBR-relative `sta mvnScbInst+2` would patch the wrong bank and
; leave the MVN src=$00 -> garbage SCB upload. phk/plb makes
; DBR:=PBR for the store, then we restore DBR := globals.
phk
plb
sta mvnScbInst+2
lda 0xE19DFE ; globals bank (long: DBR/SP-indep)
pha
plb
rep #0x20
.a16
lda bscb
lda bsFrame+0 ; scbPtr offset
tax
ldy #0x9D00
lda #199
mvnScbInst: mvn 0xE1, 0x00 ; mvn dst,src (src bank runtime-patched)
; MVN left DBR=$E1; restore DBR := globals bank (caller's,
; stashed at $E1:9DFE). NOT phk -- under multi-seg PBR is this
; asm's bank, not the globals bank. A is dead here.
sep #0x20
.a8
lda 0xE19DFE
pha
plb
rep #0x20
.a16
bsSkipScb:
; 2. Palette upload (512 bytes) via MVN, only when the palette dirty
; bit is set. Same trick.
lda bflags
lda bsFrame+8 ; uploadFlags (absolute, DBR=PBR)
and #2
beq bsSkipPal
sep #0x20
.a8
lda bpal+2
lda bsFrame+6 ; palettePtr bank byte (DBR = globals)
; Self-mod the MVN src-bank byte via DBR:=PBR (see the SCB MVN
; above) so the patch reaches the instruction in THIS code's
; bank, then restore DBR := globals.
phk
plb
sta mvnPalInst+2
lda 0xE19DFE ; globals bank (long: DBR/SP-indep)
pha
plb
rep #0x20
.a16
lda bpal
lda bsFrame+4 ; palettePtr offset
tax
ldy #0x9E00
lda #511
mvnPalInst: mvn 0xE1, 0x00
; MVN left DBR=$E1; restore DBR := globals bank (see SCB MVN).
sep #0x20
.a8
lda 0xE19DFE
pha
plb
rep #0x20
.a16
bsSkipPal:
; 3. Pixel blit via PEI-slam, with per-row dirty skip and chunked SEI.
@ -2708,7 +2863,14 @@ bsSkipPal:
sta gPeiOrigSp
sep #0x20
.a8
lda 0xc035
; Soft switches are accessed via LONG addressing to bank $E0
; ($E0Cxxx), NOT `sta $C035`. Under the GS/OS Loader DBR=$08,
; a DBR-relative `lda/sta $C0xx` hits bank-$08 RAM, not the
; I/O soft switches -- so shadow/RAMRD/RAMWRT silently never
; toggle and the slam writes nothing to $E1. Bank $E0 always
; holds the real I/O. (Technique from the prior joeylib's
; asmSlam: >$E0C035 for shadow, >$E0C0xx for RAMRD/RAMWRT.)
lda 0xE0C035
sta gPeiOrigShadow
rep #0x20
.a16
@ -2719,17 +2881,13 @@ peiChunkBegin:
ldy #0 ; Y = in-chunk row counter
sei
sep #0x20
.a8
lda gPeiOrigShadow
and #0xF1 ; clear bits 1,2,3 -> SHR shadow ON
sta 0xc035
lda #0
sta 0xc005 ; AUXWRITE on
sta 0xc003 ; RAMRD on
rep #0x20
.a16
; Shadow/AUXWRITE/RAMRD are scoped to each PEI burst below
; (peiSlamRow), NOT the whole chunk. The per-row control
; logic reads/writes bank-0 globals (dirty bands, row LUT,
; gPei* scratch, gPeiOrigSp); RAMRD/AUXWRITE on would
; misdirect those to aux bank $01 -- reading garbage dirty
; decisions and a garbage saved SP that ran RTL off to
; $FFFE. MVN rows need no switches (explicit src/dst banks).
peiRowLoop:
cpx #200
@ -2753,7 +2911,11 @@ peiCheckDirty:
brl peiRowLoop
; Dirty row: narrow spans take the exact-extent MVN; wide spans take
; the unchanged full-row PEI slam.
; the full-row PEI slam ($01 stack-push, shadowed $01->$E1). The slam is
; the GS fast path (the whole reason the stage lives at $01/2000 and the
; screen at $E1/2000); re-enabled now that the DBR/D-frame bugs that were
; corrupting its dirty-band reads are fixed, plus the shadow mask clears
; bit 4 (aux-bank shadow inhibit) so $01->$E1 shadowing is actually live.
peiRowDirty:
sep #0x20
.a8
@ -2784,6 +2946,24 @@ peiSlamRow:
adc #159
tcs ; SP = row_start + 159
; Enable shadow+AUXWRITE+RAMRD for the PEI burst ONLY.
; gPeiOrigShadow is read here while RAMRD is still off, so
; it comes from bank 0 correctly.
sep #0x20
.a8
lda gPeiOrigShadow
and #0xF7 ; clear bit 3 -> SHR shadow ON. SHR
; shadowing mirrors the $2000-9FFF
; region for BOTH banks ($00->$E0 and
; $01->$E1); the burst pushes into
; aux $01 (RAMWRT) so it lands at $E1.
sta 0xE0C035
lda #0
sta 0xE0C005 ; RAMWRT on: $00:0200-BFFF writes -> $01
sta 0xE0C003 ; RAMRD on: PEI reads aux $01 stage
rep #0x20
.a16
; 80 PEIs from DP+$9E down to DP+$00.
pei 0x9E
pei 0x9C
@ -2866,6 +3046,19 @@ peiSlamRow:
pei 0x02
pei 0x00
; Restore: AUXWRITE/RAMRD off FIRST, then read gPeiOrigShadow
; from bank 0 to restore the shadow register. After this the
; bank-0 control reads below (and at peiChunkEnd) hit main.
sep #0x20
.a8
lda #0
sta 0xE0C004 ; RAMWRT off
sta 0xE0C002 ; RAMRD off
lda gPeiOrigShadow
sta 0xE0C035 ; shadow restored
rep #0x20
.a16
lda gPeiCurRow
tax
inx
@ -2921,6 +3114,16 @@ peiMvnRow:
tay
lda gPeiMvnCount
mvn 0xE1, 0x01
; MVN left DBR=$E1; restore DBR := globals bank (caller's,
; stashed at $E1:9DFE) before the absolute gPei*/dirty reads
; below and on the next pass. A is dead (reloaded just below).
sep #0x20
.a8
lda 0xE19DFE
pha
plb
rep #0x20
.a16
; Restore row counters and advance.
lda gPeiCurRow
tax
@ -2937,10 +3140,10 @@ peiChunkEnd:
sep #0x20
.a8
lda gPeiOrigShadow
sta 0xc035
sta 0xE0C035
lda #0
sta 0xc004 ; AUXWRITE off
sta 0xc002 ; RAMRD off
sta 0xE0C004 ; RAMWRT off
sta 0xE0C002 ; RAMRD off
rep #0x20
.a16
cli
@ -3036,116 +3239,136 @@ mdrExit:
; ====================================================================
; iigsPollJoystickInner(void)
;
; Reads the IIgs paddle ports (PDL0/PDL1) at 1 MHz, ported from the old
; joeylib jIIgs.asm. Outputs gJoyPx/gJoyPy/gJoyResolved (DRAWPRIMS
; scratch). No args.
; Reads the IIgs paddle ports (PDL0/PDL1) at 1 MHz. No args; returns the
; packed result in registers (see ABI below) -- no output globals.
;
; ABI: void; no register/stack args. DBR forced to 0 for the abs $C0xx
; I/O reads (restored via PLB before return).
; ABI: no args. Returns a uint32_t in A:X (A=low16, X=high16):
; A = (JoyX << 8) | resolved , X = JoyY
; i.e. resolved = ret & 0xFF (3 = stick present, 0 = none),
; px = (ret >> 8) & 0xFF (JoyX 0..255),
; py = (ret >> 16) & 0xFF (JoyY 0..255).
; Register return is used instead of output globals because an asm WRITE
; of a global does not reach the C-read address in this toolchain.
;
; Faithful port of John Brooks' 1 MHz single-pass GetJoyXY. KEY trick:
; DP is set to $C000 so the paddle/speed I/O is read DP-relative
; (`lda $64` -> $00C064). DP is ALWAYS bank 0, so this is DBR-INDEPENDENT
; -- no DBR forcing, so DBR stays at the caller's bank ($08 under GS/OS)
; and the gJoy* globals (absolute, DBR-relative) are written in the right
; bank with no bank juggling. The dual/solo counter reads JoyX ($C064) and
; JoyY ($C065) together (`lda $64 / and $65`) every ~11 cyc; whichever
; axis trips first, it drops to a solo loop for the other. A settle-wait
; (with timeout) up front avoids a mid-discharge trigger and avoids
; hanging when no stick is attached.
; ====================================================================
.section .text.iigsPollJoystickInner,"ax"
.globl iigsPollJoystickInner
iigsPollJoystickInner:
php
sei
rep #0x30
.a16
.i16
; Save caller's DBR; force DBR=0 so abs $C0xx hits the I/O page.
phb
pea 0x0000
plb ; pull low byte -> DBR=0
plb ; pull high byte (discard)
sep #0x30 ; M=8, X=8
sep #0x34 ; sei + 8-bit M/X
.a8
.i8
phd
pea 0xC000
pld ; DP = $C000 (I/O via DP, bank 0)
; Save CYAREG and force 1 MHz (clear bit 7).
lda 0xc036
sta gJoyOrigSpeed
and #0x7F
sta 0xc036
ldx #0 ; pdl0 count (increments by 2)
ldy #0 ; pdl1 count (increments by 1; asl at end)
lda #0
sta gJoyResolved
; PTRIG: start both paddle one-shot timers.
lda 0xc070
joyChkPdl0:
lda 0xc064 ; PDL0
bpl joyGotPdl0 ; bit7=0 -> pdl0 done
inx
inx
beq joyTimeoutX ; X wrapped -> pdl0 never fired
lda 0xc065 ; PDL1
bmi joyNoGots ; bit7=1 -> pdl1 still busy
; pdl1 just fired. Mark resolved, switch to pdl0-only loop.
lda #0x02
sta gJoyResolved
joyPdl0Only:
lda 0xc064
bpl joyAllDone
inx
inx
bne joyPdl0Only
bra joyTimeoutX
joyNoGots:
; Wait for both paddles to settle (bit7=0), with a timeout so a missing
; stick can't hang the poll.
ldy #0
gjWaitBad:
lda 0x64 ; $C064 (JoyX)
ora 0x65 ; | $C065 (JoyY)
bpl gjSettled ; both bit7=0 -> settled
iny
bne joyChkPdl0
bra joyTimeoutY
bne gjWaitBad ; Y<256 -> keep waiting
brl gjNoJoy ; settle timeout -> no stick
joyGotPdl0:
lda #0x01
sta gJoyResolved
joyPdl1Only:
lda 0xc065
bpl joyAllDone
gjSettled:
bit 0x70 ; PTRIG: start X,Y timers
lsr 0x36 ; force 1 MHz (clear bit 7)
ldx #1 ; dual X,Y counter init
xba
xba
gjDualXY0:
lda 0x64
and 0x65
bpl gjToSolo
inx
gjDualXY1:
lda 0x64
and 0x65
bpl gjToSolo
inx
gjDualXY2:
lda 0x64
and 0x65
bpl gjToSolo
inx
bne gjDualXY0
gjDualXY3:
lda 0x64
and 0x65
bmi gjSameXY
dex
gjSameXY:
dex
txy
bra gjGotXY
gjSoloX:
bit 0x64
bmi gjSoloXOk
dey
bra gjGotXY
gjSoloXOk:
inx
bne gjSoloX
gjSoloY:
bit 0x65
bmi gjSoloYOk
dex
bra gjGotXY
gjToSolo:
txy
bit 0x64
bmi gjSoloXOk
bit 0x65
bpl gjSameXY
gjSoloYOk:
iny
bne joyPdl1Only
bra joyTimeoutY
bne gjSoloY
dey
joyAllDone:
lda gJoyResolved
ora #0x03
sta gJoyResolved
bra joyExit
joyTimeoutX:
lda gJoyResolved
and #0x02 ; clear bit 0 (pdl0 unresolved)
sta gJoyResolved
bra joyExit
joyTimeoutY:
lda gJoyResolved
and #0x01 ; clear bit 1 (pdl1 unresolved)
sta gJoyResolved
joyExit:
; STX/STY have no long-abs form. Stash via TXA/TYA.
txa
sta gJoyPx
tya
asl a ; scale Y from 0..127 to 0..254
sta gJoyPy
; Restore CYAREG.
lda gJoyOrigSpeed
sta 0xc036
rep #0x30
gjGotXY:
rol 0x36 ; restore CPU speed (8-bit mode)
; X = JoyX (px), Y = JoyY (py), both 0..255. Hand the result back in
; REGISTERS as the C ABI's uint32_t (A = low16, X = high16) -- NOT via
; globals: an asm WRITE of a global does not reach the C-read address in
; this toolchain (DBR/bank mismatch), whereas register return is proven
; (same path as iigsGetTickWord / iigsReadHzParam). Packing:
; A (low16) = (px << 8) | resolved X (high16) = py
; -> C sees: resolved = r&0xFF, px = (r>>8)&0xFF, py = (r>>16)&0xFF.
pld ; restore caller DP
plp ; back to caller M=I=16; X=px, Y=py
.a16
.i16
plb ; restore caller DBR
phx ; save px (16-bit)
tya
tax ; X = py -> high word of result
pla ; A = px
xba ; A = px << 8 (XBA is 16-bit reg-wide)
ora #0x0003 ; A = (px << 8) | resolved(=3)
rtl ; uint32_t in A:X
plp ; restores I from pre-SEI value
rtl
gjNoJoy:
; Settle timed out (speed unchanged, so no `rol`). Report no stick
; (resolved=0) with centred axes (px=py=128).
pld ; restore caller DP
plp ; back to caller M=I=16
.a16
.i16
ldx #128 ; py = 128 (high word)
lda #0x8000 ; (px=128 << 8) | resolved(=0)
rtl ; uint32_t in A:X
; ====================================================================
@ -3296,8 +3519,47 @@ wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit
sta wsFrame+24
lda 30,s ; maxSp
sta wsFrame+26
lda #wsFrame
tcd ; D -> wsFrame
; Copy the staged 32-byte frame into a STACK (bank-0) D-frame:
; wsFrame is a BSS global in the LOAD bank, but D (the direct
; page) is ALWAYS bank 0, so `tcd #wsFrame` aimed every
; D-relative read and [wsRow],y / [wsSpInOut],y indirect at
; bank-0:offset while the stores above wrote loadbank:offset.
; The hardware stack is bank 0, so a stack D-frame matches.
lda wsFrame+30
pha
lda wsFrame+28
pha
lda wsFrame+26
pha
lda wsFrame+24
pha
lda wsFrame+22
pha
lda wsFrame+20
pha
lda wsFrame+18
pha
lda wsFrame+16
pha
lda wsFrame+14
pha
lda wsFrame+12
pha
lda wsFrame+10
pha
lda wsFrame+8
pha
lda wsFrame+6
pha
lda wsFrame+4
pha
lda wsFrame+2
pha
lda wsFrame+0
pha
tsc
inc a ; A = SP+1
tcd ; D -> stacked frame (bank 0)
; Cache 8-bit constants used across walk + scan + fill.
sep #0x20
@ -3757,8 +4019,13 @@ wsSkipBelow:
sta [wsSpInOut],y
wsExit:
.a8
.i8
rep #0x30 ; M=16/X=16 for the SP arithmetic
.a16
.i16
tsc
clc
adc #32 ; drop the 32-byte stack D-frame
tcs
pld
plb
plp
@ -4160,26 +4427,11 @@ bsFrame:
wsFrame:
.zero 32
; iigsPollJoystickInner outputs.
.section .bss.gJoyPx,"aw"
.globl gJoyPx
gJoyPx:
.zero 2
.section .bss.gJoyPy,"aw"
.globl gJoyPy
gJoyPy:
.zero 2
.section .bss.gJoyResolved,"aw"
.globl gJoyResolved
gJoyResolved:
.zero 2
.section .bss.gJoyOrigSpeed,"aw"
.globl gJoyOrigSpeed
gJoyOrigSpeed:
.zero 2
; iigsPollJoystickInner has NO output globals: it returns its result in
; registers (uint32_t in A:X). An asm WRITE of a global does not reach the
; C-read address in this toolchain (asm READING a C-defined global like
; gStageMinWord is fine; asm WRITING one that C reads is not), so the
; register-return ABI is used instead.
; iigsFloodWalkAndScansInner scratch.
.section .bss.wsLeftX,"aw"

View file

@ -14,13 +14,13 @@ export JOEY_TOOLCHAINS
# IIgs (llvm-mos w65816: clang + llvm-mc + link816)
#
# LLVM816_ROOT is the toolchain root (clang / llvm-mc / link816 plus the
# prebuilt runtime). clang-build.sh drives compile/assemble/link. It can
# be overridden to point at a vendored copy; it currently defaults to the
# build scratchpad where the toolchain was bootstrapped.
# TODO: vendor the toolchain under toolchains/iigs.
# prebuilt runtime + MAME + ROMs). clang-build.sh drives compile/assemble/
# link. It is vendored under toolchains/iigs/llvm-mos by install.sh -- a
# git clone of the 65816-llvm-mos repo with tools/ + runtime built via the
# repo's own setup.sh. Override LLVM816_ROOT to point at a different copy.
# ------------------------------------------------------------------------
: "${LLVM816_ROOT:=/tmp/claude-1000/-home-scott-claude-joeylib/bf471cbc-f6e2-4287-86b6-58e16cc489e6/scratchpad/65816-llvm-mos}"
: "${LLVM816_ROOT:=${JOEY_TOOLCHAINS}/iigs/llvm-mos}"
export LLVM816_ROOT
export IIGS_CLANG_BUILD="${JOEY_TOOLCHAINS}/iigs/clang-build.sh"
# merlin32 assembles the NinjaTrackerPlus audio replayer (third-party asm).

View file

@ -177,8 +177,8 @@ should_install() {
}
# ------------------------------------------------------------------------
# IIgs: Merlin32 (free) + NinjaTrackerPlus audio source. The w65816
# clang/llvm-mos toolchain is referenced via LLVM816_ROOT (see env.sh).
# IIgs: llvm-mos w65816 toolchain (clang + runtime + MAME -- the primary
# build path, vendored under iigs/llvm-mos) + Merlin32 + NinjaTrackerPlus.
# ------------------------------------------------------------------------
install_audio_libxmp() {
@ -226,6 +226,46 @@ install_iigs() {
local base="${SCRIPT_DIR}/iigs"
mkdir -p "${base}"
# llvm-mos w65816 toolchain (clang + llvm-mc + link816 + the prebuilt
# runtime, plus MAME + apple2gs ROMs). The primary IIgs build path:
# clang-build.sh / make/iigs.mk resolve it via $LLVM816_ROOT, which
# toolchains/env.sh defaults to ${base}/llvm-mos. It is a git clone of
# the project repo, built by the repo's own setup.sh (clang/llc/llvm-mc
# build + link816/omfEmit + runtime + MAME). Calypsi and GNO/ME are
# optional reference targets we skip.
local llvm_dir="${base}/llvm-mos"
local llvm_repo="https://forge.duensing.digital/AI_Slop/65816-llvm-mos.git"
local llvm_clang="${llvm_dir}/tools/llvm-mos-build/bin/clang"
if is_done "iigs_llvm" && [ -x "${llvm_clang}" ]; then
ok "llvm-mos w65816 toolchain already built"
STATUS[iigs_llvm]="ok"
else
if [ ! -d "${llvm_dir}/.git" ]; then
info "Cloning 65816-llvm-mos into ${llvm_dir}"
git clone "${llvm_repo}" "${llvm_dir}" || \
INSTRUCTIONS[iigs_llvm]="git clone ${llvm_repo} ${llvm_dir} failed"
fi
if [ -f "${llvm_dir}/setup.sh" ] && [ ! -x "${llvm_clang}" ]; then
info "Building llvm-mos toolchain via setup.sh (~30-60 min: clang + runtime + MAME)"
( cd "${llvm_dir}" && bash setup.sh --skip-calypsi --skip-gno ) || \
warn "llvm-mos setup.sh reported issues; see above"
fi
if [ -x "${llvm_clang}" ]; then
mark_done "iigs_llvm"
ok "llvm-mos w65816 toolchain built at ${llvm_dir}"
STATUS[iigs_llvm]="ok"
else
STATUS[iigs_llvm]="missing"
INSTRUCTIONS[iigs_llvm]=$(cat <<EOF
llvm-mos w65816 toolchain not found at ${llvm_dir}.
Clone and build it (the clang build takes ~30-60 min):
git clone ${llvm_repo} ${llvm_dir}
cd ${llvm_dir} && ./setup.sh --skip-calypsi --skip-gno
EOF
)
fi
fi
# PHP CLI is required by tools/joeymod when converting .MOD files
# to NinjaTrackerPlus's .NTP runtime format. Installed once here so
# the IIgs audio asset pipeline works out of the box.