diff --git a/agi.c b/agi.c index 51ec225..b0a9fce 100644 --- a/agi.c +++ b/agi.c @@ -152,7 +152,10 @@ static jlSurfaceT *gBackdrop; static jlSurfaceT *gStage; static AgiVmT gVm; static LogicCacheSlotT gLogicCache[AGI_MAX_RESOURCES]; -static ViewCacheSlotT gViewCache[AGI_MAX_RESOURCES]; +// View cache: 256 slots of ~5 KB each (~1.3 MB). Far too large for the +// IIgs bank-0 BSS, so it is jlAlloc'd at startup (Memory Manager handle +// on the IIgs, malloc elsewhere) rather than declared as a static array. +static ViewCacheSlotT *gViewCache; static bool gPicReady; static uint8_t gOverrideRoom; static bool gOverrideRoomActive; @@ -1729,6 +1732,16 @@ int main(int argc, char **argv) { } } + // The view cache is too large for bank-0 BSS on the IIgs; pull it + // from the platform's large-allocator. jlAlloc does not zero its + // result, so clear it so every slot starts with loaded == false. + gViewCache = (ViewCacheSlotT *)jlAlloc(sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES); + if (gViewCache == NULL) { + jlShutdown(); + return 1; + } + memset(gViewCache, 0, sizeof(ViewCacheSlotT) * AGI_MAX_RESOURCES); + gameDir = resolveGameDir(argc, argv); startingRoom = resolveStartingRoom(argc, argv); gOverrideRoom = startingRoom; @@ -1858,6 +1871,8 @@ int main(int argc, char **argv) { jlAudioShutdown(); releaseLogicCache(); releaseViewCache(); + jlFree(gViewCache); + gViewCache = NULL; if (gBackdrop != NULL) { jlSurfaceDestroy(gBackdrop); } diff --git a/agiPic.c b/agiPic.c index 9da1796..efb216b 100644 --- a/agiPic.c +++ b/agiPic.c @@ -13,6 +13,7 @@ #include "agi.h" +#include "joey/core.h" #include "joey/draw.h" #include "surfaceInternal.h" @@ -408,8 +409,8 @@ static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) { // ----- Public API (alphabetical) ----- bool agiPicAlloc(AgiPicT *pic) { - pic->visual = (uint8_t *)malloc(AGI_PIC_PIXELS); - pic->priority = (uint8_t *)malloc(AGI_PIC_PIXELS); + pic->visual = (uint8_t *)jlAlloc(AGI_PIC_PIXELS); + pic->priority = (uint8_t *)jlAlloc(AGI_PIC_PIXELS); if (pic->visual == NULL || pic->priority == NULL) { agiPicFree(pic); return false; @@ -651,11 +652,11 @@ void agiPicAddView(AgiPicT *pic, const AgiViewT *view, void agiPicFree(AgiPicT *pic) { if (pic->visual != NULL) { - free(pic->visual); + jlFree(pic->visual); pic->visual = NULL; } if (pic->priority != NULL) { - free(pic->priority); + jlFree(pic->priority); pic->priority = NULL; } } diff --git a/agiView.c b/agiView.c index f81513f..4c75b9a 100644 --- a/agiView.c +++ b/agiView.c @@ -43,14 +43,157 @@ #endif +// ----- File-local types ----- + +// Per-draw invariants for one cel blit, grouped so they live in memory +// rather than competing for the w65816's tiny register file. Passing a +// pointer to this keeps the hot blit helpers under the backend's +// register-allocation ceiling. +typedef struct { + const uint8_t *picPriority; + int16_t x; + int16_t width; + uint8_t actorPri; + uint8_t transparent; + bool mirrored; +} AgiCelBlitT; + + // ----- Prototypes ----- +static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY); +static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow); +static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height); static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes); static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored); // ----- Internal helpers (alphabetical) ----- +// Planar fallback row walker (stage->pixels == NULL). Routes every +// opaque, visible pixel through jlDrawPixel so the port's c2p / +// plane-sync path stays correct. Split out of agiViewDraw so its +// locals don't pile onto the chunky path's register budget. +static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) { + int16_t width; + int16_t cy; + int16_t cx; + int16_t drawCx; + int16_t agiX; + int16_t agiY; + int16_t stageX; + int16_t stageY; + uint8_t rleByte; + uint8_t color; + uint8_t runLen; + uint8_t r; + uint8_t picPri; + + width = blit->width; + for (cy = 0; cy < height; cy++) { + cx = 0; + for (;;) { + rleByte = *rle++; + if (rleByte == 0u) { break; } + color = (uint8_t)(rleByte >> 4); + runLen = (uint8_t)(rleByte & 0x0Fu); + for (r = 0u; r < runLen; r++) { + if (cx >= width) { break; } + if (color != blit->transparent) { + drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx; + agiX = (int16_t)(blit->x + drawCx); + agiY = (int16_t)(topY + cy); + if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) { + picPri = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX]; + if (picPri <= blit->actorPri) { + stageX = (int16_t)(agiX << 1); + stageY = (int16_t)(destY + agiY); + jlDrawPixel(stage, stageX, stageY, color); + jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color); + } + } + } + cx++; + } + } + } +} + + +// Chunky fast-path walker for one source row. Consumes the cel's RLE +// stream from `rle`, writing pixel-doubled bytes into `stageRow` where +// the picture priority allows it, and returns the advanced RLE pointer +// so the caller can hand it to the next row. A NULL priRow / stageRow +// means the row is clipped out vertically: the RLE is still walked (so +// the pointer lands correctly for later rows) but nothing is drawn. +static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) { + int16_t width; + int16_t cx; + int16_t drawCx; + int16_t agiX; + uint8_t rleByte; + uint8_t color; + uint8_t runLen; + uint8_t packed; + uint8_t r; + + width = blit->width; + cx = 0; + for (;;) { + rleByte = *rle++; + if (rleByte == 0u) { break; } + color = (uint8_t)(rleByte >> 4); + runLen = (uint8_t)(rleByte & 0x0Fu); + // Transparent runs and vertically-clipped rows both just skip + // ahead by runLen without touching the stage. + if (color == blit->transparent || priRow == NULL) { + cx = (int16_t)(cx + runLen); + if (cx > width) { cx = width; } + continue; + } + packed = (uint8_t)((color << 4) | color); + for (r = 0u; r < runLen; r++) { + if (cx >= width) { break; } + drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx; + agiX = (int16_t)(blit->x + drawCx); + if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) { + if (priRow[agiX] <= blit->actorPri) { + stageRow[agiX] = packed; + } + } + cx++; + } + } + return rle; +} + + +// Mark the cel's stage bounding box dirty once after a chunky blit. +// Computes the pixel-doubled bbox from the source rect, clips it to the +// stage so the surface marker doesn't reject the whole rect for an +// off-screen edge, and records it if any of it is on-screen. +static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) { + int16_t minStageX; + int16_t maxStageX; + int16_t minStageY; + int16_t maxStageY; + + minStageX = (int16_t)(x << 1); + maxStageX = (int16_t)((x + width) << 1); + minStageY = (int16_t)(destY + topY); + maxStageY = (int16_t)(destY + topY + height); + if (minStageX < 0) { minStageX = 0; } + if (minStageY < 0) { minStageY = 0; } + if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; } + if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; } + if (maxStageX > minStageX && maxStageY > minStageY) { + surfaceMarkDirtyRect(stage, minStageX, minStageY, + (int16_t)(maxStageX - minStageX), + (int16_t)(maxStageY - minStageY)); + } +} + + static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) { uint8_t celCount; uint8_t i; @@ -149,37 +292,27 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, const uint8_t *picPriority, jlSurfaceT *stage, int16_t destY) { const AgiCelInfoT *cel; + AgiCelBlitT blit; const uint8_t *rle; bool mirrored; - int16_t width; int16_t height; - uint8_t transparent; int16_t topY; - int16_t cy; - int16_t cx; - int16_t drawCx; - int16_t agiX; - int16_t agiY; - int16_t stageX; - int16_t stageY; - uint8_t rleByte; - uint8_t color; - uint8_t packed; - uint8_t runLen; - uint8_t r; - uint8_t picPri; - uint8_t *stagePixels; + uint8_t *stagePixels; cel = resolveMirror(view, loopIdx, celIdx, &mirrored); if (cel == NULL) { return; } - width = (int16_t)cel->width; - height = (int16_t)cel->height; - transparent = cel->transparentColor; - rle = cel->rleData; - topY = (int16_t)(y - height + 1); - stagePixels = (stage != NULL) ? stage->pixels : NULL; + height = (int16_t)cel->height; + blit.picPriority = picPriority; + blit.x = x; + blit.width = (int16_t)cel->width; + blit.actorPri = actorPri; + blit.transparent = cel->transparentColor; + blit.mirrored = mirrored; + rle = cel->rleData; + topY = (int16_t)(y - height + 1); + stagePixels = (stage != NULL) ? stage->pixels : NULL; // Per-pixel chunky fast path: pixel-doubled output means each // source pixel maps to exactly one stage byte (both nibbles = @@ -187,10 +320,8 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, // direct, marking the cel bbox dirty once at the end. ~80x // faster on DOSBox @ 386SX-equivalent CPU cycles. if (stagePixels != NULL) { - int16_t minStageX = (int16_t)(x << 1); - int16_t maxStageX = (int16_t)((x + width) << 1); - int16_t minStageY = (int16_t)(destY + topY); - int16_t maxStageY = (int16_t)(destY + topY + height); + int16_t cy; + int16_t agiY; const uint8_t *priRow; uint8_t *stageRow; @@ -208,82 +339,15 @@ void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, stageRow = stagePixels + (int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW; } - cx = 0; - for (;;) { - rleByte = *rle++; - if (rleByte == 0u) { break; } - color = (uint8_t)(rleByte >> 4); - runLen = (uint8_t)(rleByte & 0x0Fu); - if (color == transparent) { - cx = (int16_t)(cx + runLen); - if (cx > width) { cx = width; } - continue; - } - if (priRow == NULL) { - cx = (int16_t)(cx + runLen); - if (cx > width) { cx = width; } - continue; - } - packed = (uint8_t)((color << 4) | color); - for (r = 0u; r < runLen; r++) { - if (cx >= width) { break; } - drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx; - agiX = (int16_t)(x + drawCx); - if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) { - if (priRow[agiX] <= actorPri) { - stageRow[agiX] = packed; - } - } - cx++; - } - } - } - // Dirty-rect tracking at the cel level only: we know the - // bbox a priori (the cel's stage rect), and per-pixel tight - // bounds aren't worth the inner-loop cost on a 386. Clip to - // the stage so the surface marker doesn't reject the whole - // rect for an off-screen edge. - if (minStageX < 0) { minStageX = 0; } - if (minStageY < 0) { minStageY = 0; } - if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; } - if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; } - if (maxStageX > minStageX && maxStageY > minStageY) { - surfaceMarkDirtyRect(stage, minStageX, minStageY, - (int16_t)(maxStageX - minStageX), - (int16_t)(maxStageY - minStageY)); + rle = blitRowChunky(&blit, rle, priRow, stageRow); } + markCelDirty(stage, x, blit.width, destY, topY, height); return; } // Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through // jlDrawPixel so the port's c2p / plane-sync path stays correct. - for (cy = 0; cy < height; cy++) { - cx = 0; - for (;;) { - rleByte = *rle++; - if (rleByte == 0u) { break; } - color = (uint8_t)(rleByte >> 4); - runLen = (uint8_t)(rleByte & 0x0Fu); - for (r = 0u; r < runLen; r++) { - if (cx >= width) { break; } - if (color != transparent) { - drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx; - agiX = (int16_t)(x + drawCx); - agiY = (int16_t)(topY + cy); - if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) { - picPri = picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX]; - if (picPri <= actorPri) { - stageX = (int16_t)(agiX << 1); - stageY = (int16_t)(destY + agiY); - jlDrawPixel(stage, stageX, stageY, color); - jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color); - } - } - } - cx++; - } - } - } + blitPlanar(&blit, rle, height, topY, stage, destY); } diff --git a/agiVm.c b/agiVm.c index c51cf19..c23e476 100644 --- a/agiVm.c +++ b/agiVm.c @@ -276,6 +276,17 @@ #endif +// Result of one opcode-group handler in the split agiVmRun dispatch. +// VM_DISPATCH_CONTINUE: opcode handled, advance to the next. RETURN: +// handled, stop and let agiVmRun return vm->haltReason. UNHANDLED: opcode +// is owned by a later group's handler. +typedef enum { + VM_DISPATCH_CONTINUE = 0, + VM_DISPATCH_RETURN = 1, + VM_DISPATCH_UNHANDLED = 2 +} VmDispatchE; + + // ----- Prototypes ----- static bool enterLogic(AgiVmT *vm, uint8_t logicId); @@ -291,6 +302,10 @@ static bool pushFrame(AgiVmT *vm); static bool readByte(AgiVmT *vm, uint8_t *out); static bool readU16(AgiVmT *vm, uint16_t *out); static bool skipTestArgs(AgiVmT *vm, uint8_t testOp); +static VmDispatchE vmStepCore(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepMisc(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepObject(AgiVmT *vm, uint8_t op); +static VmDispatchE vmStepText(AgiVmT *vm, uint8_t op); static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded); @@ -1027,20 +1042,47 @@ void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLen AgiVmHaltE agiVmRun(AgiVmT *vm) { - uint8_t op; - uint8_t a; - uint8_t b; - uint8_t c; - uint8_t d; - uint8_t e; - uint8_t f; - uint8_t g; + uint8_t op; + VmDispatchE res; while (vm->haltReason == AGI_VM_HALT_NONE) { if (!readByte(vm, &op)) { // readByte sets haltReason = TRUNCATED on out-of-range. return vm->haltReason; } + res = vmStepCore(vm, op); + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepObject(vm, op); + } + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepText(vm, op); + } + if (res == VM_DISPATCH_UNHANDLED) { + res = vmStepMisc(vm, op); + } + if (res == VM_DISPATCH_RETURN) { + return vm->haltReason; + } + } + return vm->haltReason; +} + + +// Opcode dispatch is partitioned across four __attribute__((noinline)) +// handlers so the llvm-mos w65816 register allocator is never asked to +// colour the whole 1200-line switch at once (it ran out of registers when +// it was a single function). Each handler returns VM_DISPATCH_CONTINUE +// (advance to the next opcode), VM_DISPATCH_RETURN (stop; agiVmRun returns +// vm->haltReason), or VM_DISPATCH_UNHANDLED (opcode is owned by a later +// group). vmStepMisc owns the default fallback and never returns UNHANDLED. +// Behaviour is byte-for-byte identical to the original monolithic switch. +// +// Group 1: RETURN, arithmetic/flags (0x00-0x11), IF/GOTO, new.room, +// load.logic, call, and the picture-cache opcodes (0x12-0x1C). +static VmDispatchE __attribute__((noinline)) vmStepCore(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + switch (op) { case OP_RETURN: if (vm->callDepth > 0u) { @@ -1052,7 +1094,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_INCREMENT: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } // AGI spec: increment clamps at 255. if (vm->vars[a] != 0xFFu) { @@ -1062,7 +1104,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DECREMENT: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } // AGI spec: decrement clamps at 0 (does NOT wrap). // Sparkle counters like v221..v224 stay at 0 once @@ -1075,42 +1117,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_ASSIGNN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = b; break; case OP_ASSIGNV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = vm->vars[b]; break; case OP_ADDN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] + b); break; case OP_ADDV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] + vm->vars[b]); break; case OP_SUBN: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] - b); break; case OP_SUBV: if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] - vm->vars[b]); break; @@ -1118,7 +1160,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_LINDIRECTV: // var[a] = var[var[b]] if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[a] = vm->vars[vm->vars[b]]; break; @@ -1126,7 +1168,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_RINDIRECT: // var[var[a]] = var[b] if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[vm->vars[a]] = vm->vars[b]; break; @@ -1134,68 +1176,68 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_LINDIRECTN: // var[var[a]] = b (immediate) if (!readByte(vm, &a) || !readByte(vm, &b)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->vars[vm->vars[a]] = b; break; case OP_SET: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = 1u; break; case OP_RESET: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = 0u; break; case OP_TOGGLE: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[a] = (uint8_t)(vm->flags[a] ? 0u : 1u); break; case OP_SETV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = 1u; break; case OP_RESETV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = 0u; break; case OP_TOGGLEV: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->flags[vm->vars[a]] = (uint8_t)(vm->flags[vm->vars[a]] ? 0u : 1u); break; case OP_IF: if (!executeIf(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_GOTO: if (!executeGoto(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_NEW_ROOM: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->newRoomId = a; vm->haltReason = AGI_VM_HALT_NEW_ROOM; @@ -1206,7 +1248,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_NEW_ROOM_V: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } vm->newRoomId = vm->vars[a]; vm->haltReason = AGI_VM_HALT_NEW_ROOM; @@ -1221,7 +1263,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // bytecode swap happens at OP_CALL time. We touch the // callback so callers that prefetch can react. if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.fetchLogic != NULL) { uint8_t logicId; @@ -1236,27 +1278,27 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CALL: case OP_CALL_V: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } { uint8_t logicId; logicId = (op == OP_CALL) ? a : vm->vars[a]; if (!pushFrame(vm)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (!enterLogic(vm, logicId)) { // Roll back the pushed frame so RETURN doesn't // pop garbage state. vm->callDepth--; - return vm->haltReason; + return VM_DISPATCH_RETURN; } } break; case OP_LOAD_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.loadPic != NULL) { vm->callbacks.loadPic(vm->callbacks.ctx, vm->vars[a]); @@ -1265,7 +1307,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DRAW_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.drawPic != NULL) { vm->callbacks.drawPic(vm->callbacks.ctx, vm->vars[a]); @@ -1280,7 +1322,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DISCARD_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.discardPic != NULL) { vm->callbacks.discardPic(vm->callbacks.ctx, vm->vars[a]); @@ -1289,24 +1331,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_OVERLAY_PIC: if (!readByte(vm, &a)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.overlayPic != NULL) { vm->callbacks.overlayPic(vm->callbacks.ctx, vm->vars[a]); } break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 2: object table (set.view/loop/cel/priority, draw/erase, get.posn, +// etc.), the lumped view-cache and reposition aliases (0x94/0x99), +// object-collision/block opcodes, animation cycling, and motion (0x1E-0x5B). +static VmDispatchE __attribute__((noinline)) vmStepObject(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + + switch (op) { // ----- view cache hints (no-op; host loads on demand) ----- case OP_LOAD_VIEW: case OP_LOAD_VIEW_V: case OP_DISCARD_VIEW: case OP_DISCARD_VIEW_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; // ----- object table ----- case OP_ANIMATE_OBJ: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_ANIMATE_OBJ %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { agiObjReset(&vm->objects[a]); @@ -1320,7 +1380,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_DRAW: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_DRAW %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_DRAWN; @@ -1328,7 +1388,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_ERASE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } jlLogF("OP_ERASE %u", (unsigned)a); if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_DRAWN; @@ -1337,7 +1397,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_POSITION: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_POSITION %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); if (a < AGI_MAX_OBJECTS) { @@ -1349,7 +1409,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_POSITION_V: case OP_REPOSITION_TO_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].x = (int16_t)vm->vars[b]; @@ -1362,7 +1422,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_GET_POSN: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->vars[b] = (uint8_t)vm->objects[a].x; @@ -1373,7 +1433,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_REPOSITION: // reposition(N, vdx, vdy) — apply signed deltas (vars hold int8_t-style) if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { int8_t dx = (int8_t)vm->vars[b]; @@ -1385,7 +1445,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_REPOSITION_TO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_REPOSITION_TO %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c); if (a < AGI_MAX_OBJECTS) { @@ -1395,7 +1455,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_VIEW obj=%u view=%u", (unsigned)a, (unsigned)b); if (a < AGI_MAX_OBJECTS) { vm->objects[a].viewId = b; @@ -1404,7 +1464,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_VIEW_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_VIEW_V obj=%u view=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1414,7 +1474,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_LOOP obj=%u loop=%u", (unsigned)a, (unsigned)b); if (a < AGI_MAX_OBJECTS) { vm->objects[a].loop = b; @@ -1423,7 +1483,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_LOOP_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_LOOP_V obj=%u loop=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1433,27 +1493,27 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_FIX_LOOP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_LOOP_FIXED; } break; case OP_RELEASE_LOOP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_LOOP_FIXED; } break; case OP_SET_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = b; vm->objects[a].cycleTick = 0u; } break; case OP_SET_CEL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = vm->vars[b]; vm->objects[a].cycleTick = 0u; } break; case OP_LAST_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && vm->callbacks.viewCelCount != NULL) { uint8_t c = vm->callbacks.viewCelCount(vm->callbacks.ctx, vm->objects[a].viewId, @@ -1465,22 +1525,22 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_CURRENT_CEL: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].cel : 0u; break; case OP_CURRENT_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].loop : 0u; break; case OP_CURRENT_VIEW: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].viewId : 0u; break; case OP_NUMBER_OF_LOOPS: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && vm->callbacks.viewLoopCount != NULL) { uint8_t c = vm->callbacks.viewLoopCount(vm->callbacks.ctx, vm->objects[a].viewId); @@ -1491,7 +1551,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].priority = b; vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; @@ -1499,7 +1559,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_PRIORITY_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].priority = vm->vars[b]; vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED; @@ -1507,42 +1567,42 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_RELEASE_PRIORITY: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_PRI_FIXED; } break; case OP_GET_PRIORITY: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].priority : 0u; break; case OP_STOP_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_UPDATING; } break; case OP_START_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_UPDATING; } break; case OP_FORCE_UPDATE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } // Force-redraw hint; we always redraw, so no-op apart from arg consume. break; case OP_IGNORE_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_HORIZON_IGN; } break; case OP_OBSERVE_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_HORIZON_IGN; } break; case OP_SET_HORIZON: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } vm->horizon = a; break; @@ -1551,16 +1611,16 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_OBJECT_ON_ANYTHING: case OP_IGNORE_BLOCKS: case OP_OBSERVE_BLOCKS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_IGNORE_OBJS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_OBJS_IGN; } break; case OP_OBSERVE_OBJS: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_OBJS_IGN; } break; @@ -1569,7 +1629,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { int16_t dy; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS && b < AGI_MAX_OBJECTS) { dx = (int16_t)(vm->objects[a].x - vm->objects[b].x); @@ -1586,7 +1646,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_BLOCK: // 4 args: (x1, y1, x2, y2) if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; @@ -1595,17 +1655,17 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- animation cycling ----- case OP_STOP_CYCLING: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING; } break; case OP_START_CYCLING: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; } break; case OP_NORMAL_CYCLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_NORMAL; vm->objects[a].endOfLoopFlag = 0u; @@ -1614,7 +1674,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_END_OF_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_END_LOOP; vm->objects[a].endOfLoopFlag = b; @@ -1624,7 +1684,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_REVERSE_CYCLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_REVERSE; vm->objects[a].endOfLoopFlag = 0u; @@ -1633,7 +1693,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_REVERSE_LOOP: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleMode = AGI_CYCLE_REV_LOOP; vm->objects[a].endOfLoopFlag = b; @@ -1643,7 +1703,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_CYCLE_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].cycleTime = vm->vars[b]; if (vm->objects[a].cycleTime == 0u) { vm->objects[a].cycleTime = 1u; } @@ -1653,12 +1713,12 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- motion ----- case OP_STOP_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].direction = 0u; } break; case OP_START_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { if (a == 0u) { vm->objects[0].direction = vm->vars[ENG_VAR_EGO_DIR]; @@ -1669,7 +1729,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_STEP_SIZE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].stepSize = vm->vars[b]; if (vm->objects[a].stepSize == 0u) { vm->objects[a].stepSize = 1u; } @@ -1677,7 +1737,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_STEP_TIME: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].stepTime = vm->vars[b]; if (vm->objects[a].stepTime == 0u) { vm->objects[a].stepTime = 1u; } @@ -1688,7 +1748,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_MOVE_OBJ: // n, x, y, vstep, flag if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_MOVE_OBJ %u -> (%u,%u) step=%u doneFlag=%u", (unsigned)a, (unsigned)b, (unsigned)c, (unsigned)d, (unsigned)e); @@ -1705,7 +1765,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_MOVE_OBJ_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_MOVE_OBJ_V %u -> v%u/v%u=(%u,%u) vstep=v%u=%u doneFlag=%u", (unsigned)a, (unsigned)b, (unsigned)c, @@ -1724,7 +1784,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_FOLLOW_EGO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].followStep = b; @@ -1734,7 +1794,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_WANDER: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_WANDER; vm->objects[a].wanderTick = 0u; @@ -1742,12 +1802,12 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_NORMAL_MOTION: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_NORMAL; } break; case OP_SET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SET_DIR obj=%u dir=v%u (=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->vars[b]); if (a < AGI_MAX_OBJECTS) { @@ -1757,37 +1817,54 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_GET_DIR: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].direction : 0u; break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 3: inventory state (0x5C-0x61), sound (0x62-0x64), and the text +// opcodes (0x65-0x76, plus the 0x9A clear.text.rect alias lumped here). +static VmDispatchE __attribute__((noinline)) vmStepText(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + + switch (op) { // ----- inventory (state only; reads OBJECT data via host later) ----- case OP_GET: case OP_DROP: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_GET_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_PUT: case OP_PUT_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; case OP_GET_ROOM_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = 0u; break; // ----- sound (no-op; complete callback fires immediately) ----- case OP_LOAD_SOUND: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SOUND: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } jlLogF("OP_SOUND id=%u flag=%u (prev playing=%u prevFlag=%u)", (unsigned)a, (unsigned)b, (unsigned)vm->soundPlaying, (unsigned)vm->soundDoneFlag); @@ -1848,24 +1925,24 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- text ----- case OP_PRINT: { char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_PRINT_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_DISPLAY: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, c), expanded, sizeof(expanded)); jlLogF("OP_DISPLAY row=%u col=%u msg=%u text=\"%s\"", @@ -1877,7 +1954,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_DISPLAY_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[c]), expanded, sizeof(expanded)); writeTextRow(vm, vm->vars[a], vm->vars[b], expanded); @@ -1887,7 +1964,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CLEAR_LINES: { uint8_t r; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } jlLogF("OP_CLEAR_LINES rows=%u..%u bg=%u", (unsigned)a, (unsigned)b, (unsigned)c); @@ -1903,7 +1980,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_CLEAR_TEXT_RECT: { uint8_t r; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (c >= AGI_TEXT_ROWS) c = AGI_TEXT_ROWS - 1u; for (r = a; r <= c; r++) { @@ -1918,22 +1995,22 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_CURSOR_CHAR: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_TEXT_ATTRIBUTE: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->textFg = a; vm->textBg = b; break; case OP_SHAKE_SCREEN: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_CONFIGURE_SCREEN: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; @@ -1949,7 +2026,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { char expanded[AGI_STRING_LEN + 1u]; uint8_t i; - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (a < AGI_MAX_STRINGS) { expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, b), expanded, sizeof(expanded)); for (i = 0u; i < (uint8_t)AGI_STRING_LEN; i++) { @@ -1965,23 +2042,44 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // get.string(s, msgId, row, col, max) — parser input; // no parser yet, just consume args. if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_WORD_TO_STRING: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; case OP_PARSE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_GET_NUM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[b] = 0u; break; + default: + return VM_DISPATCH_UNHANDLED; + } + return VM_DISPATCH_CONTINUE; +} + + +// Group 4: input (0x77-0x79), add.to.pic (0x7A-0x7B), the misc/system +// opcodes, print.at aliases, menus, and mul/div (0x7C-0xAA). Owns the +// default fallback (kActionArgBytes stub-consume / unknown-op halt), so it +// never returns VM_DISPATCH_UNHANDLED. +static VmDispatchE __attribute__((noinline)) vmStepMisc(AgiVmT *vm, uint8_t op) { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t e; + uint8_t f; + uint8_t g; + + switch (op) { // ----- input ----- case OP_PREVENT_INPUT: vm->acceptInput = false; @@ -1995,7 +2093,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { uint8_t i; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) { if (vm->keyBindings[i].active == 0u) { @@ -2012,7 +2110,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { // ----- picture composition ----- case OP_ADD_TO_PIC: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.addToPic != NULL) { vm->callbacks.addToPic(vm->callbacks.ctx, a, b, c, d, e, f, g); @@ -2021,7 +2119,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_ADD_TO_PIC_V: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } if (vm->callbacks.addToPic != NULL) { vm->callbacks.addToPic(vm->callbacks.ctx, @@ -2043,15 +2141,15 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_RESTART_GAME: vm->haltReason = AGI_VM_HALT_QUIT; - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_SHOW_OBJ: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_RANDOM: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } { uint16_t span; @@ -2069,20 +2167,20 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_OBJ_STATUS_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_QUIT: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } vm->haltReason = AGI_VM_HALT_QUIT; - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_SHOW_MEM: break; case OP_PAUSE: openPrintModal(vm, "Game Paused.\n\nPress ENTER to continue."); - return vm->haltReason; + return VM_DISPATCH_RETURN; case OP_ECHO_LINE: case OP_CANCEL_LINE: @@ -2098,15 +2196,15 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SCRIPT_SIZE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_GAME_ID: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_LOG: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_SET_SCAN_START: @@ -2118,37 +2216,37 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { case OP_TRACE_INFO: if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } break; case OP_PRINT_AT: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_PRINT_AT_V: { char expanded[AGI_PRINT_MAX_LEN + 1u]; if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded)); openPrintModal(vm, expanded); - return vm->haltReason; + return VM_DISPATCH_RETURN; } case OP_SET_UPPER_LEFT: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } break; // ----- menus ----- case OP_SET_MENU: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } if (vm->menuCount < AGI_MAX_MENUS) { vm->menus[vm->menuCount].nameMsgId = a; vm->menus[vm->menuCount].itemCount = 0u; @@ -2157,7 +2255,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_MENU_ITEM: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (vm->menuCount > 0u) { AgiMenuT *m = &vm->menus[vm->menuCount - 1u]; if (m->itemCount < AGI_MAX_MENU_ITEMS) { @@ -2178,7 +2276,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { uint8_t i; uint8_t enable; - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } enable = (op == OP_ENABLE_ITEM) ? 1u : 0u; for (m = 0u; m < vm->menuCount; m++) { for (i = 0u; i < vm->menus[m].itemCount; i++) { @@ -2195,7 +2293,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SHOW_OBJ_V: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; case OP_OPEN_DIALOGUE: @@ -2203,24 +2301,24 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_MUL_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] * b); break; case OP_MUL_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } vm->vars[a] = (uint8_t)(vm->vars[a] * vm->vars[b]); break; case OP_DIV_N: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (b != 0u) { vm->vars[a] = (uint8_t)(vm->vars[a] / b); } break; case OP_DIV_V: - if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; } + if (!readByte(vm, &a) || !readByte(vm, &b)) { return VM_DISPATCH_RETURN; } if (vm->vars[b] != 0u) { vm->vars[a] = (uint8_t)(vm->vars[a] / vm->vars[b]); } @@ -2231,7 +2329,7 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { break; case OP_SET_SIMPLE: - if (!readByte(vm, &a)) { return vm->haltReason; } + if (!readByte(vm, &a)) { return VM_DISPATCH_RETURN; } break; default: { @@ -2248,14 +2346,13 @@ AgiVmHaltE agiVmRun(AgiVmT *vm) { } for (i = 0u; i < argBytes; i++) { if (!readByte(vm, &scratch)) { - return vm->haltReason; + return VM_DISPATCH_RETURN; } } break; } } - } - return vm->haltReason; + return VM_DISPATCH_CONTINUE; }