diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c index 2186701..86a2ca6 100644 --- a/examples/adventure/adventure.c +++ b/examples/adventure/adventure.c @@ -1452,75 +1452,73 @@ 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; } } +} + + +// 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) { + case 0: ny = (int16_t)(cy - 1); break; + case 1: nx = (int16_t)(cx + 1); break; + case 2: ny = (int16_t)(cy + 1); break; + default: nx = (int16_t)(cx - 1); break; + } + if (!walkable(room, nx, ny)) { + return; + } + if (gBfsParent[ny][nx] != 0xFF) { + return; + } + gBfsParent[ny][nx] = d; + 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 */ + gBfsParent[scy][scx] = 0xFE; // sentinel: start while (qhead < qtail) { cx = gBfsQueueX[qhead]; @@ -1530,68 +1528,120 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) break; } for (d = 0; d < 4; d++) { - nx = cx; - ny = cy; - switch (d) { - case 0: ny = (int16_t)(cy - 1); break; - case 1: nx = (int16_t)(cx + 1); break; - case 2: ny = (int16_t)(cy + 1); break; - default: nx = (int16_t)(cx - 1); break; - } - if (!walkable(room, nx, ny)) { - continue; - } - if (gBfsParent[ny][nx] != 0xFF) { - continue; - } - gBfsParent[ny][nx] = d; - gBfsQueueX[qtail] = nx; - gBfsQueueY[qtail] = ny; - qtail++; + bfsExpand(room, cx, cy, d, &qtail); } } - if (gBfsParent[tcy][tcx] == 0xFF) { + return gBfsParent[tcy][tcx] != 0xFF; +} + + +// 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; + cy = tcy; + while (n < MAX_PATH_NODES) { + pathRevX[n] = cx; + pathRevY[n] = cy; + n++; + d = gBfsParent[cy][cx]; + if (d == 0xFE) { + break; + } + switch (d) { + case 0: cy = (int16_t)(cy + 1); break; + case 1: cx = (int16_t)(cx - 1); break; + case 2: cy = (int16_t)(cy - 1); break; + default: cx = (int16_t)(cx + 1); break; + } + } + + gPathLen = 0; + for (i = (int16_t)(n - 1); i >= 0; i--) { + 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; + } + } + 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; } - /* Walk back from target to start, prepending to the path. */ - { - int16_t pathRevX[MAX_PATH_NODES]; - int16_t pathRevY[MAX_PATH_NODES]; - int16_t n; - int16_t i; - - n = 0; - cx = tcx; - cy = tcy; - while (n < MAX_PATH_NODES) { - pathRevX[n] = cx; - pathRevY[n] = cy; - n++; - d = gBfsParent[cy][cx]; - if (d == 0xFE) { break; } - switch (d) { - case 0: cy = (int16_t)(cy + 1); break; - case 1: cx = (int16_t)(cx - 1); break; - case 2: cy = (int16_t)(cy - 1); break; - default: cx = (int16_t)(cx + 1); break; - } - } + // 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; - for (i = (int16_t)(n - 1); i >= 0; i--) { - int16_t px, py; - cellToFoot(pathRevX[i], pathRevY[i], &px, &py); - gPathX[gPathLen] = px; - gPathY[gPathLen] = py; - gPathLen++; - if (gPathLen >= MAX_PATH_NODES) { break; } - } - gPathStep = 0; + return false; } + buildPath(tcx, tcy); return gPathLen > 0; } diff --git a/examples/agi/agi.c b/examples/agi/agi.c index 51ec225..b0a9fce 100644 --- a/examples/agi/agi.c +++ b/examples/agi/agi.c @@ -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); } diff --git a/examples/agi/agiPic.c b/examples/agi/agiPic.c index 9da1796..efb216b 100644 --- a/examples/agi/agiPic.c +++ b/examples/agi/agiPic.c @@ -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; } } diff --git a/examples/agi/agiView.c b/examples/agi/agiView.c index f81513f..4c75b9a 100644 --- a/examples/agi/agiView.c +++ b/examples/agi/agiView.c @@ -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,37 +292,27 @@ 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; + 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; - rle = cel->rleData; - topY = (int16_t)(y - height + 1); - stagePixels = (stage != NULL) ? stage->pixels : NULL; + height = (int16_t)cel->height; + 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; // Per-pixel chunky fast path: pixel-doubled output means each // source pixel maps to exactly one stage byte (both nibbles = @@ -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); } diff --git a/examples/agi/agiVm.c b/examples/agi/agiVm.c index c51cf19..c23e476 100644 --- a/examples/agi/agiVm.c +++ b/examples/agi/agiVm.c @@ -276,6 +276,17 @@ #endif +// Result of one opcode-group handler in the split agiVmRun dispatch. +// VM_DISPATCH_CONTINUE: opcode handled, advance to the next. RETURN: +// handled, stop and let agiVmRun return vm->haltReason. UNHANDLED: opcode +// is owned by a later group's handler. +typedef enum { + VM_DISPATCH_CONTINUE = 0, + VM_DISPATCH_RETURN = 1, + VM_DISPATCH_UNHANDLED = 2 +} VmDispatchE; + + // ----- Prototypes ----- static bool enterLogic(AgiVmT *vm, uint8_t logicId); @@ -291,6 +302,10 @@ static bool pushFrame(AgiVmT *vm); static bool readByte(AgiVmT *vm, uint8_t *out); static bool readU16(AgiVmT *vm, uint16_t *out); static bool skipTestArgs(AgiVmT *vm, uint8_t testOp); +static VmDispatchE vmStepCore(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepMisc(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepText(AgiVmT *vm, uint8_t op); static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded); @@ -1027,20 +1042,47 @@ void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLen AgiVmHaltE agiVmRun(AgiVmT *vm) { - uint8_t op; - uint8_t a; - uint8_t b; - uint8_t c; - uint8_t d; - uint8_t e; - uint8_t f; - uint8_t g; + uint8_t op; + VmDispatchE res; while (vm->haltReason == AGI_VM_HALT_NONE) { if (!readByte(vm, &op)) { // readByte sets haltReason = TRUNCATED on out-of-range. return vm->haltReason; } + res = vmStepCore(vm, op); + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepObject(vm, op); + } + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepText(vm, op); + } + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepMisc(vm, op); + } + if (res == VM_DISPATCH_RETURN) { + return vm->haltReason; + } + } + return vm->haltReason; +} + + +// Opcode dispatch is partitioned across four __attribute__((noinline)) +// handlers so the llvm-mos w65816 register allocator is never asked to +// colour the whole 1200-line switch at once (it ran out of registers when +// it was a single function). Each handler returns VM_DISPATCH_CONTINUE +// (advance to the next opcode), VM_DISPATCH_RETURN (stop; agiVmRun returns +// vm->haltReason), or VM_DISPATCH_UNHANDLED (opcode is owned by a later +// group). vmStepMisc owns the default fallback and never returns UNHANDLED. +// Behaviour is byte-for-byte identical to the original monolithic switch. +// +// Group 1: RETURN, arithmetic/flags (0x00-0x11), IF/GOTO, new.room, +// load.logic, call, and the picture-cache opcodes (0x12-0x1C). +static VmDispatchE __attribute__((noinline)) vmStepCore(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + switch (op) { case OP_RETURN: if (vm->callDepth > 0u) { @@ -1052,7 +1094,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_INCREMENT: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } // AGI spec: increment clamps at 255. if (vm->vars[a] != 0xFFu) { @@ -1062,7 +1104,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DECREMENT: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } // AGI spec: decrement clamps at 0 (does NOT wrap). // Sparkle counters like v221..v224 stay at 0 once @@ -1075,42 +1117,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_ASSIGNN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = b; break; case OP_ASSIGNV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = vm->vars[b]; break; case OP_ADDN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] + b); break; case OP_ADDV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] + vm->vars[b]); break; case OP_SUBN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] - b); break; case OP_SUBV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] - vm->vars[b]); break; @@ -1118,7 +1160,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_LINDIRECTV: // var[a] = var[var[b]] if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = vm->vars[vm->vars[b]]; break; @@ -1126,7 +1168,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_RINDIRECT: // var[var[a]] = var[b] if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[vm->vars[a]] = vm->vars[b]; break; @@ -1134,68 +1176,68 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_LINDIRECTN: // var[var[a]] = b (immediate) if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[vm->vars[a]] = b; break; case OP_SET: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = 1u; break; case OP_RESET: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = 0u; break; case OP_TOGGLE: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = (uint8_t)(vm->flags[a] ? 0u : 1u); break; case OP_SETV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = 1u; break; case OP_RESETV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = 0u; break; case OP_TOGGLEV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = (uint8_t)(vm->flags[vm->vars[a]] ? 0u : 1u); break; case OP_IF: if (!executeIf(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_GOTO: if (!executeGoto(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_NEW_ROOM: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->newRoomId = a; vm->haltReason = AGI_VM_HALT_NEW_ROOM; @@ -1206,7 +1248,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_NEW_ROOM_V: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->newRoomId = vm->vars[a]; vm->haltReason = AGI_VM_HALT_NEW_ROOM; @@ -1221,7 +1263,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // bytecode swap happens at OP_CALL time. We touch the // callback so callers that prefetch can react. if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.fetchLogic != NULL) { uint8_t logicId; @@ -1236,27 +1278,27 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CALL: case OP_CALL_V: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } { uint8_t logicId; logicId = (op == OP_CALL) ? a : vm->vars[a]; if (!pushFrame(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (!enterLogic(vm, logicId)) { // Roll back the pushed frame so RETURN doesn't // pop garbage state. vm->callDepth--; - return vm->haltReason; + return VM_DISPATCH_RETURN; } } break; case OP_LOAD_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.loadPic != NULL) { vm->callbacks.loadPic(vm->callbacks.ctx, vm->vars[a]); @@ -1265,7 +1307,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DRAW_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.drawPic != NULL) { vm->callbacks.drawPic(vm->callbacks.ctx, vm->vars[a]); @@ -1280,7 +1322,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DISCARD_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.discardPic != NULL) { vm->callbacks.discardPic(vm->callbacks.ctx, vm->vars[a]); @@ -1289,24 +1331,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_OVERLAY_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.overlayPic != NULL) { vm->callbacks.overlayPic(vm->callbacks.ctx, vm->vars[a]); } break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 2: object table (set.view/loop/cel/priority, draw/erase, get.posn, +// etc.), the lumped view-cache and reposition aliases (0x94/0x99), +// object-collision/block opcodes, animation cycling, and motion (0x1E-0x5B). +static VmDispatchE __attribute__((noinline)) vmStepObject(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + + switch (op) { // ----- view cache hints (no-op; host loads on demand) ----- case OP_LOAD_VIEW: case OP_LOAD_VIEW_V: case OP_DISCARD_VIEW: case OP_DISCARD_VIEW_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; // ----- object table ----- case OP_ANIMATE_OBJ: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_ANIMATE_OBJ %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { agiObjReset(&vm->objects[a]); @@ -1320,7 +1380,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_DRAW: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_DRAW %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_DRAWN; @@ -1328,7 +1388,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_ERASE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_ERASE %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_DRAWN; @@ -1337,7 +1397,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_POSITION: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_POSITION %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); if (a < AGI_MAX_OBJECTS) { @@ -1349,7 +1409,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_POSITION_V: case OP_REPOSITION_TO_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].x = (int16_t)vm->vars[b]; @@ -1362,7 +1422,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_GET_POSN: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->vars[b] = (uint8_t)vm->objects[a].x; @@ -1373,7 +1433,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_REPOSITION: // reposition(N, vdx, vdy) — apply signed deltas (vars hold int8_t-style) if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { int8_t dx = (int8_t)vm->vars[b]; @@ -1385,7 +1445,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_REPOSITION_TO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_REPOSITION_TO %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); if (a < AGI_MAX_OBJECTS) { @@ -1395,7 +1455,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_VIEW obj=%u view=%u", (unsigned)a, (unsigned)b); if (a < AGI_MAX_OBJECTS) { vm->objects[a].viewId = b; @@ -1404,7 +1464,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_VIEW_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_VIEW_V obj=%u view=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1414,7 +1474,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_LOOP obj=%u loop=%u", (unsigned)a, (unsigned)b); if (a < AGI_MAX_OBJECTS) { vm->objects[a].loop = b; @@ -1423,7 +1483,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_LOOP_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_LOOP_V obj=%u loop=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1433,27 +1493,27 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_FIX_LOOP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_LOOP_FIXED; } break; case OP_RELEASE_LOOP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_LOOP_FIXED; } break; case OP_SET_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = b; vm->objects[a].cycleTick = 0u; } break; case OP_SET_CEL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = vm->vars[b]; vm->objects[a].cycleTick = 0u; } break; case OP_LAST_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && vm->callbacks.viewCelCount != NULL) { uint8_t c = vm->callbacks.viewCelCount(vm->callbacks.ctx, vm->objects[a].viewId, @@ -1465,22 +1525,22 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_CURRENT_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].cel : 0u; break; case OP_CURRENT_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].loop : 0u; break; case OP_CURRENT_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].viewId : 0u; break; case OP_NUMBER_OF_LOOPS: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && vm->callbacks.viewLoopCount != NULL) { uint8_t c = vm->callbacks.viewLoopCount(vm->callbacks.ctx, vm->objects[a].viewId); @@ -1491,7 +1551,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].priority = b; vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; @@ -1499,7 +1559,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_PRIORITY_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].priority = vm->vars[b]; vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; @@ -1507,42 +1567,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_RELEASE_PRIORITY: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_PRI_FIXED; } break; case OP_GET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].priority : 0u; break; case OP_STOP_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_UPDATING; } break; case OP_START_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_UPDATING; } break; case OP_FORCE_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } // Force-redraw hint; we always redraw, so no-op apart from arg consume. break; case OP_IGNORE_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_HORIZON_IGN; } break; case OP_OBSERVE_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_HORIZON_IGN; } break; case OP_SET_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } vm->horizon = a; break; @@ -1551,16 +1611,16 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_OBJECT_ON_ANYTHING: case OP_IGNORE_BLOCKS: case OP_OBSERVE_BLOCKS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_IGNORE_OBJS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_OBJS_IGN; } break; case OP_OBSERVE_OBJS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_OBJS_IGN; } break; @@ -1569,7 +1629,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { int16_t dy; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && b < AGI_MAX_OBJECTS) { dx = (int16_t)(vm->objects[a].x - vm->objects[b].x); @@ -1586,7 +1646,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_BLOCK: // 4 args: (x1, y1, x2, y2) if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; @@ -1595,17 +1655,17 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- animation cycling ----- case OP_STOP_CYCLING: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING; } break; case OP_START_CYCLING: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; } break; case OP_NORMAL_CYCLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_NORMAL; vm->objects[a].endOfLoopFlag = 0u; @@ -1614,7 +1674,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_END_OF_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_END_LOOP; vm->objects[a].endOfLoopFlag = b; @@ -1624,7 +1684,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_REVERSE_CYCLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_REVERSE; vm->objects[a].endOfLoopFlag = 0u; @@ -1633,7 +1693,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_REVERSE_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_REV_LOOP; vm->objects[a].endOfLoopFlag = b; @@ -1643,7 +1703,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_CYCLE_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleTime = vm->vars[b]; if (vm->objects[a].cycleTime == 0u) { vm->objects[a].cycleTime = 1u; } @@ -1653,12 +1713,12 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- motion ----- case OP_STOP_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].direction = 0u; } break; case OP_START_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { if (a == 0u) { vm->objects[0].direction = vm->vars[ENG_VAR_EGO_DIR]; @@ -1669,7 +1729,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_STEP_SIZE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].stepSize = vm->vars[b]; if (vm->objects[a].stepSize == 0u) { vm->objects[a].stepSize = 1u; } @@ -1677,7 +1737,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_STEP_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].stepTime = vm->vars[b]; if (vm->objects[a].stepTime == 0u) { vm->objects[a].stepTime = 1u; } @@ -1688,7 +1748,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_MOVE_OBJ: // n, x, y, vstep, flag if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_MOVE_OBJ %u -> (%u,%u) step=%u doneFlag=%u", (unsigned)a, (unsigned)b, (unsigned)c, (unsigned)d, (unsigned)e); @@ -1705,7 +1765,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_MOVE_OBJ_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_MOVE_OBJ_V %u -> v%u/v%u=(%u,%u) vstep=v%u=%u doneFlag=%u", (unsigned)a, (unsigned)b, (unsigned)c, @@ -1724,7 +1784,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_FOLLOW_EGO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].followStep = b; @@ -1734,7 +1794,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_WANDER: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_WANDER; vm->objects[a].wanderTick = 0u; @@ -1742,12 +1802,12 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_NORMAL_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_NORMAL; } break; case OP_SET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_DIR obj=%u dir=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1757,37 +1817,54 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_GET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].direction : 0u; break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 3: inventory state (0x5C-0x61), sound (0x62-0x64), and the text +// opcodes (0x65-0x76, plus the 0x9A clear.text.rect alias lumped here). +static VmDispatchE __attribute__((noinline)) vmStepText(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + + switch (op) { // ----- inventory (state only; reads OBJECT data via host later) ----- case OP_GET: case OP_DROP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_GET_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_PUT: case OP_PUT_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; case OP_GET_ROOM_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = 0u; break; // ----- sound (no-op; complete callback fires immediately) ----- case OP_LOAD_SOUND: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SOUND: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SOUND id=%u flag=%u (prev playing=%u prevFlag=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->soundPlaying, (unsigned)vm->soundDoneFlag); @@ -1848,24 +1925,24 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- text ----- case OP_PRINT: { char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_PRINT_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_DISPLAY: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, c), expanded, sizeof(expanded)); jlLogF("OP_DISPLAY row=%u col=%u msg=%u text=\"%s\"", @@ -1877,7 +1954,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DISPLAY_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[c]), expanded, sizeof(expanded)); writeTextRow(vm, vm->vars[a], vm->vars[b], expanded); @@ -1887,7 +1964,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CLEAR_LINES: { uint8_t r; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_CLEAR_LINES rows=%u..%u bg=%u", (unsigned)a, (unsigned)b, (unsigned)c); @@ -1903,7 +1980,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CLEAR_TEXT_RECT: { uint8_t r; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (c >= AGI_TEXT_ROWS) c = AGI_TEXT_ROWS - 1u; for (r = a; r <= c; r++) { @@ -1918,22 +1995,22 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_CURSOR_CHAR: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_TEXT_ATTRIBUTE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->textFg = a; vm->textBg = b; break; case OP_SHAKE_SCREEN: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_CONFIGURE_SCREEN: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; @@ -1949,7 +2026,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { char expanded[AGI_STRING_LEN + 1u]; uint8_t i; - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_STRINGS) { expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, b), expanded, sizeof(expanded)); for (i = 0u; i < (uint8_t)AGI_STRING_LEN; i++) { @@ -1965,23 +2042,44 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // get.string(s, msgId, row, col, max) — parser input; // no parser yet, just consume args. if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_WORD_TO_STRING: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; case OP_PARSE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_GET_NUM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = 0u; break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 4: input (0x77-0x79), add.to.pic (0x7A-0x7B), the misc/system +// opcodes, print.at aliases, menus, and mul/div (0x7C-0xAA). Owns the +// default fallback (kActionArgBytes stub-consume / unknown-op halt), so it +// never returns VM_DISPATCH_UNHANDLED. +static VmDispatchE __attribute__((noinline)) vmStepMisc(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + uint8_t f; + uint8_t g; + + switch (op) { // ----- input ----- case OP_PREVENT_INPUT: vm->acceptInput = false; @@ -1995,7 +2093,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { uint8_t i; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) { if (vm->keyBindings[i].active == 0u) { @@ -2012,7 +2110,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- picture composition ----- case OP_ADD_TO_PIC: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.addToPic != NULL) { vm->callbacks.addToPic(vm->callbacks.ctx, a, b, c, d, e, f, g); @@ -2021,7 +2119,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_ADD_TO_PIC_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.addToPic != NULL) { vm->callbacks.addToPic(vm->callbacks.ctx, @@ -2043,15 +2141,15 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_RESTART_GAME: vm->haltReason = AGI_VM_HALT_QUIT; - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_SHOW_OBJ: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_RANDOM: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } { uint16_t span; @@ -2069,20 +2167,20 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_OBJ_STATUS_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_QUIT: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } vm->haltReason = AGI_VM_HALT_QUIT; - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_SHOW_MEM: break; case OP_PAUSE: openPrintModal(vm, "Game Paused.\n\nPress ENTER to continue."); - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_ECHO_LINE: case OP_CANCEL_LINE: @@ -2098,15 +2196,15 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SCRIPT_SIZE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_GAME_ID: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_LOG: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_SCAN_START: @@ -2118,37 +2216,37 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_TRACE_INFO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_PRINT_AT: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_PRINT_AT_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_SET_UPPER_LEFT: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; // ----- menus ----- case OP_SET_MENU: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (vm->menuCount < AGI_MAX_MENUS) { vm->menus[vm->menuCount].nameMsgId = a; vm->menus[vm->menuCount].itemCount = 0u; @@ -2157,7 +2255,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_MENU_ITEM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (vm->menuCount > 0u) { AgiMenuT *m = &vm->menus[vm->menuCount - 1u]; if (m->itemCount < AGI_MAX_MENU_ITEMS) { @@ -2178,7 +2276,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { uint8_t i; uint8_t enable; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } enable = (op == OP_ENABLE_ITEM) ? 1u : 0u; for (m = 0u; m < vm->menuCount; m++) { for (i = 0u; i < vm->menus[m].itemCount; i++) { @@ -2195,7 +2293,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SHOW_OBJ_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_OPEN_DIALOGUE: @@ -2203,24 +2301,24 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_MUL_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] * b); break; case OP_MUL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] * vm->vars[b]); break; case OP_DIV_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (b != 0u) { vm->vars[a] = (uint8_t)(vm->vars[a] / b); } break; case OP_DIV_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (vm->vars[b] != 0u) { vm->vars[a] = (uint8_t)(vm->vars[a] / vm->vars[b]); } @@ -2231,7 +2329,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_SIMPLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; default: { @@ -2248,14 +2346,13 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { } for (i = 0u; i < argBytes; i++) { if (!readByte(vm, &scratch)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } } break; } } - } - return vm->haltReason; + return VM_DISPATCH_CONTINUE; } diff --git a/examples/uber/uber.c b/examples/uber/uber.c index d78ac75..1611223 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -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; - uint16_t pal[16]; - int i; - uint16_t startFrame; +// 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; + + 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); diff --git a/include/joey/core.h b/include/joey/core.h index 3c53de0..caa89bb 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -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 diff --git a/make/iigs.mk b/make/iigs.mk index 696e8b7..5461f1a 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -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) diff --git a/src/codegen/spriteCompile.c b/src/codegen/spriteCompile.c index dad4699..0e5f6cf 100644 --- a/src/codegen/spriteCompile.c +++ b/src/codegen/spriteCompile.c @@ -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; - uint32_t fnAddr; + uint8_t shift; + uint8_t *dstRow0; + uint32_t fnAddr; + void (*drawFn)(uint8_t *); - { - uint8_t *destPtr; - uint32_t destAddr; - const uint8_t *destB; - 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]; - 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" - ); + // 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); + dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)]; + fnAddr = codegenArenaBaseAddr() + + sp->slot->offset + + (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW]; + 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 diff --git a/src/codegen/spriteEmitIigs.c b/src/codegen/spriteEmitIigs.c index f34b93e..f973d9c 100644 --- a/src/codegen/spriteEmitIigs.c +++ b/src/codegen/spriteEmitIigs.c @@ -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++] = 0x20; - } - - // Epilogue: rtl (large memory model -b uses JSL/RTL). - out[cursor++] = 0x6B; + out[cursor++] = 0xC2; // REP #$20 ; M=16 + out[cursor++] = 0x20; + out[cursor++] = 0xAB; // PLB ; restore caller DBR + out[cursor++] = 0x6B; // RTL return cursor; } diff --git a/src/core/hal.h b/src/core/hal.h index ca720de..255164f 100644 --- a/src/core/hal.h +++ b/src/core/hal.h @@ -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 diff --git a/src/core/init.c b/src/core/init.c index dfc2850..de45b06 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -5,6 +5,7 @@ // jlShutdown tears those down in reverse order. #include +#include #include #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(); diff --git a/src/core/sprite.c b/src/core/sprite.c index 466aa6d..3cbe3e7 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -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) { diff --git a/src/port/iigs/audio_full.c b/src/port/iigs/audio_full.c index 702ed13..2ec8a21 100644 --- a/src/port/iigs/audio_full.c +++ b/src/port/iigs/audio_full.c @@ -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 #include // Memory Manager via . 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; - return false; + 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(); + } + } } diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c index cd4b4c6..0388e59 100644 --- a/src/port/iigs/hal.c +++ b/src/port/iigs/hal.c @@ -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 +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); } diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c index 42e0270..57006f5 100644 --- a/src/port/iigs/input.c +++ b/src/port/iigs/input.c @@ -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; diff --git a/src/port/iigs/joeyDraw.s b/src/port/iigs/joeyDraw.s index 2de396b..a1e3fa8 100644 --- a/src/port/iigs/joeyDraw.s +++ b/src/port/iigs/joeyDraw.s @@ -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" diff --git a/toolchains/env.sh b/toolchains/env.sh index 76a161a..b3a512d 100644 --- a/toolchains/env.sh +++ b/toolchains/env.sh @@ -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). diff --git a/toolchains/install.sh b/toolchains/install.sh index 7e7d94c..7e2e142 100755 --- a/toolchains/install.sh +++ b/toolchains/install.sh @@ -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 <