Lots of fixes. Maybe.
This commit is contained in:
parent
de280b7a32
commit
1972f6df90
19 changed files with 1617 additions and 939 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
make/iigs.mk
40
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// jlShutdown tears those down in reverse order.
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/core.h"
|
||||
|
|
@ -44,8 +45,40 @@ void coreSetError(const char *message) {
|
|||
}
|
||||
|
||||
|
||||
// ----- Default large-allocation backing -----
|
||||
//
|
||||
// jlAlloc / jlFree route through halBigAlloc / halBigFree. Most ports
|
||||
// have a normal heap, so the default below (plain malloc / free) serves
|
||||
// them and they do NOT need to implement the hook themselves. Only ports
|
||||
// whose C heap is too small or per-allocation-capped to satisfy a large
|
||||
// request -- currently just the IIgs -- provide their own halBigAlloc /
|
||||
// halBigFree (src/port/iigs/hal.c) and compile out this default.
|
||||
#ifndef JOEYLIB_PLATFORM_IIGS
|
||||
|
||||
void *halBigAlloc(uint32_t bytes) {
|
||||
return malloc((size_t)bytes);
|
||||
}
|
||||
|
||||
|
||||
void halBigFree(void *p) {
|
||||
free(p);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
void *jlAlloc(uint32_t bytes) {
|
||||
return halBigAlloc(bytes);
|
||||
}
|
||||
|
||||
|
||||
void jlFree(void *p) {
|
||||
halBigFree(p);
|
||||
}
|
||||
|
||||
|
||||
bool jlInit(const jlConfigT *config) {
|
||||
clearError();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
// code from non-AUDIO binaries, which pay only for the data
|
||||
// references they actually use.
|
||||
//
|
||||
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time
|
||||
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
|
||||
// iigs.mk xxd-i header rule. halAudioInit BlockMoves those bytes into
|
||||
// a fixed-bank Memory Manager handle and JSLs into them through the
|
||||
// self-modifying call stub below; NTP is bank-internal / position-
|
||||
// independent so it runs at whatever address Memory Manager picked.
|
||||
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time to
|
||||
// ntpplayer.bin and LOADED FROM DISK at runtime (NTP_PATH). halAudioInit
|
||||
// reads those bytes into a fixed-bank Memory Manager handle and JSLs into
|
||||
// them through the self-modifying call stub below; NTP is bank-internal /
|
||||
// position-independent so it runs at whatever address Memory Manager picked.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Memory Manager via <iigs/toolbox.h>. Handles/pointers are void* and the
|
||||
|
|
@ -31,12 +31,13 @@ typedef void *Pointer;
|
|||
#include "hal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
// The 34 KB NTP replayer bytes live in their own data segment
|
||||
// (build/iigs/audio/ntpdata.asm, auto-generated from ntpplayer.bin by
|
||||
// make/iigs.mk). We just extern the symbols here.
|
||||
|
||||
extern const unsigned char gNtpPlayerBytes[];
|
||||
extern const unsigned long gNtpPlayerBytes_len;
|
||||
// The ~34 KB NTP replayer is LOADED FROM DISK at runtime, not baked into
|
||||
// this TU. As rodata it overflows the IIgs bank-0 ceiling, and clang's near
|
||||
// (IMM16 + $00BE load-bank) form for &blob cannot reach a far rodata segment
|
||||
// in another bank -- so a baked blob is impossible on the IIgs regardless of
|
||||
// link816 splitting. ntpplayer.bin must ship alongside the application and is
|
||||
// resolved against the current GS/OS prefix.
|
||||
#define NTP_PATH "ntpplayer"
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
|
|
@ -142,7 +143,10 @@ static uint16_t gSfxSlotRateHz[JOEY_AUDIO_SFX_SLOTS] = { 0 };
|
|||
// 08: A9 lo hi LDA #A
|
||||
// 0B: 22 lo hi bk JSL target
|
||||
// 0F: 6B RTL
|
||||
static unsigned char gCallStub[16];
|
||||
// GLOBAL (not static): referenced by name from the inline `jsl gCallStub`
|
||||
// blocks below; link816 resolves .text relocations only against global
|
||||
// symbols, so a file-local buffer links as "unresolved gCallStub".
|
||||
unsigned char gCallStub[16];
|
||||
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
|
@ -169,6 +173,8 @@ static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) {
|
|||
static bool loadNTP(void) {
|
||||
Handle h;
|
||||
Pointer p;
|
||||
FILE *fp;
|
||||
size_t got;
|
||||
|
||||
h = NewHandle(NTP_BUFFER_BYTES, _ownerid,
|
||||
attrFixed | attrLocked | attrPage | attrNoCross,
|
||||
|
|
@ -183,7 +189,19 @@ static bool loadNTP(void) {
|
|||
return false;
|
||||
}
|
||||
|
||||
BlockMove((Pointer)gNtpPlayerBytes, p, gNtpPlayerBytes_len);
|
||||
// Read the replayer from disk (NTP_PATH) into the locked, bank-internal
|
||||
// handle -- see the NTP_PATH note above for why it is not baked in.
|
||||
fp = fopen(NTP_PATH, "rb");
|
||||
if (fp == NULL) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
}
|
||||
got = fread(p, 1, NTP_BUFFER_BYTES, fp);
|
||||
fclose(fp);
|
||||
if (got == 0) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
gNTPHandle = h;
|
||||
gNTPBase = (uint32_t)p;
|
||||
|
|
@ -417,36 +435,89 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
|
|||
}
|
||||
|
||||
|
||||
// IIgs has the Ensoniq DOC + a $C030 toggle for the system beep.
|
||||
// A tone-mode driver could program a DOC channel with a perfect
|
||||
// square wave OR pulse $C030 in a timer ISR. Not wired yet --
|
||||
// AGI-on-IIgs will be silent until this is filled in.
|
||||
void halAudioTone(uint16_t freqHz) {
|
||||
(void)freqHz;
|
||||
// PSG-style tone/voice synthesis. A PSG square-wave voice is just a
|
||||
// synthesized square sample (re)triggered on one of the SFX slots, which
|
||||
// already cooperate with the NTP MOD engine (SFX uses DOC oscillators
|
||||
// 14-21; NTP uses the lower ones). Loudness comes from the sample
|
||||
// amplitude, so no per-oscillator DOC volume plumbing is needed; the
|
||||
// host re-asserts active voices each frame via halAudioFrameTick (the
|
||||
// registered tick fn), which sustains them between the one-shot triggers.
|
||||
#define TONE_RATE 26000U // DOC playback rate for synthesized PSG
|
||||
#define TONE_BUF_BYTES 2048 // ~2 frames of square wave @ TONE_RATE
|
||||
#define AGI_ATTEN_OFF 15U // AGI attenuation: 0 = loudest .. 15 = silent
|
||||
#define IIGS_FRAME_HZ 60U
|
||||
|
||||
static int8_t gToneBuf[TONE_BUF_BYTES];
|
||||
static jlAudioTickFnT gTickFn = (jlAudioTickFnT)0;
|
||||
static uint16_t gTickReload = 1;
|
||||
static uint16_t gTickCount = 1;
|
||||
|
||||
|
||||
// Fill gToneBuf with whole periods of a signed-8-bit square wave at the
|
||||
// given half-period (samples per half-cycle) and amplitude; returns the
|
||||
// byte length written (a whole number of periods so re-triggers seam).
|
||||
static uint16_t toneFill(uint16_t halfPeriod, int8_t amp) {
|
||||
uint16_t period;
|
||||
uint16_t len;
|
||||
uint16_t i;
|
||||
|
||||
if (halfPeriod == 0) {
|
||||
halfPeriod = 1;
|
||||
}
|
||||
period = (uint16_t)(halfPeriod * 2u);
|
||||
len = (uint16_t)((TONE_BUF_BYTES / period) * period);
|
||||
if (len == 0) {
|
||||
len = (period <= TONE_BUF_BYTES) ? period : TONE_BUF_BYTES;
|
||||
}
|
||||
for (i = 0; i < len; i++) {
|
||||
gToneBuf[i] = ((uint16_t)(i % period) < halfPeriod) ? amp : (int8_t)(-(int)amp);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
// 3-voice PSG-style tone: full IIgs implementation would dedicate
|
||||
// three DOC oscillators to looping a tiny square waveform from DOC
|
||||
// RAM, programming each channel's frequency divisor + volume. The
|
||||
// existing NTPstreamsound NTP engine uses all 32 oscillators for
|
||||
// MOD playback, so a voice-mode driver needs its own oscillator
|
||||
// allocation that backs off when NTP is playing. Stubbed for now;
|
||||
// AGI-on-IIgs is silent until this lands.
|
||||
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
|
||||
(void)voice;
|
||||
(void)freqHz;
|
||||
(void)atten;
|
||||
uint16_t halfPeriod;
|
||||
uint16_t len;
|
||||
int8_t amp;
|
||||
|
||||
if (voice >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
if (freqHz == 0u || atten >= AGI_ATTEN_OFF) {
|
||||
halAudioStopSfx(voice); // freq 0 / max atten = silence
|
||||
return;
|
||||
}
|
||||
// AGI attenuation 0..14 -> amplitude 127..8 (linear; loudness is in the
|
||||
// sample, the DOC plays the slot at full volume).
|
||||
amp = (int8_t)(127 - ((uint16_t)atten * 119u) / (AGI_ATTEN_OFF - 1u));
|
||||
halfPeriod = (uint16_t)((TONE_RATE / 2u) / freqHz);
|
||||
len = toneFill(halfPeriod, amp);
|
||||
// gToneBuf is shared across voices and its contents change every call,
|
||||
// so bust halAudioPlaySfx's (ptr,len,rate) copy-cache to force a refresh.
|
||||
gSfxSlotSample[voice] = (const uint8_t *)0;
|
||||
halAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE);
|
||||
}
|
||||
|
||||
|
||||
void halAudioTone(uint16_t freqHz) {
|
||||
halAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0
|
||||
}
|
||||
|
||||
|
||||
// Frame-synced tick. The IIgs VBL is ~60 Hz and AGI sound is 60 Hz, so we
|
||||
// ride the host's per-frame jlAudioFrameTick pump rather than install a
|
||||
// raw VBL ISR (which would fight GS/OS heartbeat tasks). hz > 60 clamps to
|
||||
// per-frame; lower rates divide down.
|
||||
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
|
||||
// IIgs VBL interrupt at $C019 would be the natural hook, but
|
||||
// it's capped to ~60 Hz and AGI sound is already 60 Hz, so the
|
||||
// wiring is a no-op when the voice driver above goes silent.
|
||||
(void)fn;
|
||||
(void)hz;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@
|
|||
#include "hal.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
// Memory Manager surface for halBigAlloc / halBigFree (jlAlloc backing).
|
||||
// Same idiom as src/port/iigs/audio_full.c: Handles/pointers are void*,
|
||||
// the owner ID comes from MMStartUp(), and the attribute bit values are
|
||||
// Apple's Memory Manager constants. Only the bits we use are defined.
|
||||
#include <iigs/toolbox.h>
|
||||
typedef void *Handle;
|
||||
#define _ownerid (MMStartUp())
|
||||
#define attrFixed 0x4000
|
||||
#define attrLocked 0x8000
|
||||
// Leading bookkeeping slot: jlAlloc/jlFree traffic in plain pointers but
|
||||
// DisposeHandle needs the Handle, so we over-allocate by one Handle and
|
||||
// stash it just before the pointer we return.
|
||||
#define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle))
|
||||
|
||||
/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick
|
||||
* ($2503) and returns the low 16 bits of the system's tick counter
|
||||
* (firmware VBL ISR-driven). Polling $C019 from C user code missed
|
||||
|
|
@ -82,7 +96,7 @@ extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1
|
|||
// The asm performs the upload directly rather than a C-side memcpy,
|
||||
// which is unreliable when called from halPresent (DBR-state quirk
|
||||
// after prior asm primitives).
|
||||
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
|
||||
extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags);
|
||||
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
|
||||
// read back by halFastFloodWalkAndScans.
|
||||
extern uint16_t gFloodSeedMatch;
|
||||
|
|
@ -204,6 +218,46 @@ static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) {
|
|||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
// Large/persistent allocation via the Memory Manager. The IIgs C heap
|
||||
// is bank-0-only and a single malloc is capped near 32 KB, so buffers
|
||||
// like the AGI view cache (~1.3 MB) and the PIC visual/priority buffers
|
||||
// must be NewHandle'd. We request attrFixed|attrLocked so the block
|
||||
// neither moves nor purges while the caller holds the dereferenced
|
||||
// pointer. The 65816 has no alignment fault, so the BIG_ALLOC_HDR-byte
|
||||
// offset applied to the master pointer is safe for any access pattern.
|
||||
// The returned memory is not zero-filled (matches malloc semantics).
|
||||
void *halBigAlloc(uint32_t bytes) {
|
||||
Handle h;
|
||||
uint8_t *base;
|
||||
|
||||
h = NewHandle((unsigned long)(bytes + BIG_ALLOC_HDR), _ownerid,
|
||||
(unsigned short)(attrFixed | attrLocked), NULL);
|
||||
if (h == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
HLock(h);
|
||||
base = *(uint8_t **)h;
|
||||
if (base == NULL) {
|
||||
DisposeHandle(h);
|
||||
return NULL;
|
||||
}
|
||||
// Stash the Handle in the leading slot so halBigFree can recover it.
|
||||
*(Handle *)base = h;
|
||||
return base + BIG_ALLOC_HDR;
|
||||
}
|
||||
|
||||
|
||||
void halBigFree(void *p) {
|
||||
Handle h;
|
||||
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
h = *(Handle *)((uint8_t *)p - BIG_ALLOC_HDR);
|
||||
DisposeHandle(h);
|
||||
}
|
||||
|
||||
|
||||
bool halInit(const jlConfigT *config) {
|
||||
(void)config;
|
||||
gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
|
||||
|
|
@ -232,22 +286,23 @@ const char *halLastError(void) {
|
|||
|
||||
|
||||
void halPresent(const jlSurfaceT *src) {
|
||||
uint16_t uploadFlags;
|
||||
|
||||
if (src == NULL) {
|
||||
return;
|
||||
}
|
||||
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
|
||||
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
|
||||
// C-side memcpy to bank $E1 has been unreliable from
|
||||
// halPresent's calling context, so we route everything through
|
||||
// the asm path. uploadFlags gates the optional SCB / palette
|
||||
// uploads on the dirty bits so we don't re-push 712 unchanged
|
||||
// bytes every frame: bit 0 = SCB, bit 1 = palette (pixels always).
|
||||
uploadFlags = (uint16_t)gStageScbDirty | ((uint16_t)gStagePaletteDirty << 1);
|
||||
iigsBlitStageToShr(src->scb, &src->palette[0][0], uploadFlags);
|
||||
gStagePaletteDirty = false;
|
||||
gStageScbDirty = false;
|
||||
// SCB + palette upload via the C-side memcpy to bank $E1, then pixels.
|
||||
// The asm palette MVN in iigsBlitStageToShr never wrote $E1:9E00 -- the
|
||||
// SCB MVN landed but colors showed the GS/OS default (pixels/shapes were
|
||||
// correct, every color wrong). The C memcpy is reliable now that the
|
||||
// far-const-pointer bank-drop miscompile is fixed (it formerly hit bank
|
||||
// $00). uploadScbAndPaletteIfNeeded clears the dirty bits, so the asm
|
||||
// blit (uploadFlags == 0) does pixels only. The asm's now-unreached
|
||||
// SCB/palette MVN code is dead -- a removal candidate.
|
||||
// KNOWN RESIDUAL: on a DRAW the PEI-slam corrupts $E1:9E20+ (palettes
|
||||
// 1-15; palette 0 and pixels stay correct) -- a peislam.s shadow / soft-
|
||||
// switch spill, independent of upload order. Single-palette apps are
|
||||
// unaffected; multi-palette colors above index 0 need the slam fix.
|
||||
uploadScbAndPaletteIfNeeded(src);
|
||||
iigsBlitStageToShr(src->scb, &src->palette[0][0], 0);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -177,8 +177,8 @@ should_install() {
|
|||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# IIgs: Merlin32 (free) + NinjaTrackerPlus audio source. The w65816
|
||||
# clang/llvm-mos toolchain is referenced via LLVM816_ROOT (see env.sh).
|
||||
# IIgs: llvm-mos w65816 toolchain (clang + runtime + MAME -- the primary
|
||||
# build path, vendored under iigs/llvm-mos) + Merlin32 + NinjaTrackerPlus.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
install_audio_libxmp() {
|
||||
|
|
@ -226,6 +226,46 @@ install_iigs() {
|
|||
local base="${SCRIPT_DIR}/iigs"
|
||||
mkdir -p "${base}"
|
||||
|
||||
# llvm-mos w65816 toolchain (clang + llvm-mc + link816 + the prebuilt
|
||||
# runtime, plus MAME + apple2gs ROMs). The primary IIgs build path:
|
||||
# clang-build.sh / make/iigs.mk resolve it via $LLVM816_ROOT, which
|
||||
# toolchains/env.sh defaults to ${base}/llvm-mos. It is a git clone of
|
||||
# the project repo, built by the repo's own setup.sh (clang/llc/llvm-mc
|
||||
# build + link816/omfEmit + runtime + MAME). Calypsi and GNO/ME are
|
||||
# optional reference targets we skip.
|
||||
local llvm_dir="${base}/llvm-mos"
|
||||
local llvm_repo="https://forge.duensing.digital/AI_Slop/65816-llvm-mos.git"
|
||||
local llvm_clang="${llvm_dir}/tools/llvm-mos-build/bin/clang"
|
||||
if is_done "iigs_llvm" && [ -x "${llvm_clang}" ]; then
|
||||
ok "llvm-mos w65816 toolchain already built"
|
||||
STATUS[iigs_llvm]="ok"
|
||||
else
|
||||
if [ ! -d "${llvm_dir}/.git" ]; then
|
||||
info "Cloning 65816-llvm-mos into ${llvm_dir}"
|
||||
git clone "${llvm_repo}" "${llvm_dir}" || \
|
||||
INSTRUCTIONS[iigs_llvm]="git clone ${llvm_repo} ${llvm_dir} failed"
|
||||
fi
|
||||
if [ -f "${llvm_dir}/setup.sh" ] && [ ! -x "${llvm_clang}" ]; then
|
||||
info "Building llvm-mos toolchain via setup.sh (~30-60 min: clang + runtime + MAME)"
|
||||
( cd "${llvm_dir}" && bash setup.sh --skip-calypsi --skip-gno ) || \
|
||||
warn "llvm-mos setup.sh reported issues; see above"
|
||||
fi
|
||||
if [ -x "${llvm_clang}" ]; then
|
||||
mark_done "iigs_llvm"
|
||||
ok "llvm-mos w65816 toolchain built at ${llvm_dir}"
|
||||
STATUS[iigs_llvm]="ok"
|
||||
else
|
||||
STATUS[iigs_llvm]="missing"
|
||||
INSTRUCTIONS[iigs_llvm]=$(cat <<EOF
|
||||
llvm-mos w65816 toolchain not found at ${llvm_dir}.
|
||||
Clone and build it (the clang build takes ~30-60 min):
|
||||
git clone ${llvm_repo} ${llvm_dir}
|
||||
cd ${llvm_dir} && ./setup.sh --skip-calypsi --skip-gno
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
fi
|
||||
|
||||
# PHP CLI is required by tools/joeymod when converting .MOD files
|
||||
# to NinjaTrackerPlus's .NTP runtime format. Installed once here so
|
||||
# the IIgs audio asset pipeline works out of the box.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue