joeyagi/tests/testAgiPipeline.c

284 lines
8.7 KiB
C

// Host-side AGI pipeline test (Phase 1 sub-D).
//
// Wires up a real logic-resource cache + trace callbacks and runs
// the VM in a simulated game-cycle loop:
// * agiVmRun()
// * NEW_ROOM -> update var[0]/var[1], reset flags, restart logic.0
// * RETURN -> count one cycle, restart logic.0
// * UNKNOWN_OP -> print and exit
// Capped at MAX_CYCLES so a logic that always returns without
// painting can't loop forever.
//
// What this lets us verify before touching any emulator:
// - logic.0's prologue + IF branching against fresh game state
// - room transition handling (new.room(N) loads logic N)
// - room logic execution (load.pic/draw.pic/show.pic callbacks)
// - call.logic / RETURN frame management
//
// Usage:
// testAgiPipeline <game-directory> [max-cycles]
#include "agi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_CYCLES_DEFAULT 20u
#define AGI_LOGIC_HDR_BYTES 2u
typedef struct {
uint8_t *raw;
uint16_t rawLength;
const uint8_t *bytecode;
uint16_t bytecodeLength;
} CachedLogicT;
static AgiGameT gGame;
static CachedLogicT gLogicCache[AGI_MAX_RESOURCES];
static uint32_t gTraceLoadPic;
static uint32_t gTraceDrawPic;
static uint32_t gTraceShowPic;
static uint32_t gTraceDiscardPic;
static uint32_t gTraceOverlayPic;
static uint32_t gTraceFetchLogic;
static void cbDiscardPic(void *ctx, uint8_t picId);
static void cbDrawPic(void *ctx, uint8_t picId);
static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength);
static void cbLoadPic(void *ctx, uint8_t picId);
static void cbOverlayPic(void *ctx, uint8_t picId);
static void cbShowPic(void *ctx);
static const char *haltName(AgiVmHaltE r);
static void releaseCache(void);
static void cbDiscardPic(void *ctx, uint8_t picId) {
(void)ctx;
gTraceDiscardPic++;
printf(" [host] discardPic(%u)\n", (unsigned)picId);
}
static void cbDrawPic(void *ctx, uint8_t picId) {
(void)ctx;
gTraceDrawPic++;
printf(" [host] drawPic(%u)\n", (unsigned)picId);
}
static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) {
CachedLogicT *slot;
uint8_t *raw;
uint16_t rawLength;
uint16_t bcLen;
(void)ctx;
slot = &gLogicCache[logicId];
if (slot->bytecode != NULL) {
*outLength = slot->bytecodeLength;
return slot->bytecode;
}
if (logicId >= gGame.resCount[AGI_RES_LOGIC]) {
return NULL;
}
raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength);
if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) {
if (raw != NULL) {
free(raw);
}
return NULL;
}
bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) {
free(raw);
return NULL;
}
slot->raw = raw;
slot->rawLength = rawLength;
slot->bytecode = raw + AGI_LOGIC_HDR_BYTES;
slot->bytecodeLength = bcLen;
gTraceFetchLogic++;
printf(" [host] fetchLogic(%u) -> %u bytes (first load)\n",
(unsigned)logicId, (unsigned)bcLen);
*outLength = bcLen;
return slot->bytecode;
}
static void cbLoadPic(void *ctx, uint8_t picId) {
(void)ctx;
gTraceLoadPic++;
printf(" [host] loadPic(%u)\n", (unsigned)picId);
}
static void cbOverlayPic(void *ctx, uint8_t picId) {
(void)ctx;
gTraceOverlayPic++;
printf(" [host] overlayPic(%u)\n", (unsigned)picId);
}
static void cbShowPic(void *ctx) {
(void)ctx;
gTraceShowPic++;
printf(" [host] showPic\n");
}
static const char *haltName(AgiVmHaltE r) {
switch (r) {
case AGI_VM_HALT_NONE: return "NONE";
case AGI_VM_HALT_RETURN: return "RETURN";
case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP";
case AGI_VM_HALT_TRUNCATED: return "TRUNCATED";
case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM";
case AGI_VM_HALT_NO_LOGIC: return "NO_LOGIC";
case AGI_VM_HALT_STACK_OVER: return "STACK_OVER";
default: return "?";
}
}
static void releaseCache(void) {
uint16_t i;
for (i = 0u; i < AGI_MAX_RESOURCES; i++) {
if (gLogicCache[i].raw != NULL) {
free(gLogicCache[i].raw);
gLogicCache[i].raw = NULL;
}
}
}
int main(int argc, char **argv) {
AgiVmT vm;
AgiVmCallbacksT cb;
uint16_t length;
const uint8_t *logic0;
uint32_t maxCycles;
uint32_t cycles;
AgiVmHaltE reason;
uint8_t prevRoom;
uint8_t i;
if (argc < 2 || argc > 3) {
fprintf(stderr, "usage: %s <game-directory> [max-cycles]\n", argv[0]);
return 2;
}
maxCycles = (argc == 3) ? (uint32_t)atoi(argv[2]) : (uint32_t)MAX_CYCLES_DEFAULT;
if (maxCycles == 0u) {
maxCycles = (uint32_t)MAX_CYCLES_DEFAULT;
}
if (!agiResOpen(&gGame, argv[1])) {
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
return 1;
}
memset(gLogicCache, 0, sizeof(gLogicCache));
cb.fetchLogic = cbFetchLogic;
cb.loadPic = cbLoadPic;
cb.drawPic = cbDrawPic;
cb.showPic = cbShowPic;
cb.discardPic = cbDiscardPic;
cb.overlayPic = cbOverlayPic;
cb.haveKey = NULL;
cb.fetchMessage = NULL;
cb.addToPic = NULL;
cb.soundDuration = NULL;
cb.playSound = NULL;
cb.stopSound = NULL;
cb.ctx = NULL;
agiVmInit(&vm);
agiVmSetCallbacks(&vm, &cb);
length = 0u;
logic0 = cbFetchLogic(NULL, 0u, &length);
if (logic0 == NULL) {
fprintf(stderr, "FAIL: could not load logic.0\n");
releaseCache();
agiResClose(&gGame);
return 1;
}
agiVmResetToLogic(&vm, logic0, length, 0u);
cycles = 0u;
for (;;) {
if (cycles >= maxCycles) {
printf(" (capped at %u cycles)\n", (unsigned)maxCycles);
break;
}
cycles++;
// Simulate one wall-clock second per cycle so per-second
// game logic (countdowns, state machines, animation cadence)
// advances under the host test without waiting in real time.
// KQ3's title state machine in particular is gated entirely
// on var 11 changing; without this tick logic.45's countdowns
// never fire and the title appears to stall at 20+ cycles.
agiVmTickSeconds(&vm, 1u);
printf(" cycle %u: pc=%u/%u logic=%u callDepth=%u\n",
(unsigned)cycles, (unsigned)vm.pc, (unsigned)vm.codeLength,
(unsigned)vm.currentLogicId, (unsigned)vm.callDepth);
reason = agiVmRun(&vm);
printf(" halt=%s at pc=%u logic=%u",
haltName(reason), (unsigned)vm.pc, (unsigned)vm.currentLogicId);
if (reason == AGI_VM_HALT_UNKNOWN_OP) {
printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp);
}
if (reason == AGI_VM_HALT_NEW_ROOM) {
printf(" newRoom=%u", (unsigned)vm.newRoomId);
}
printf("\n");
if (reason == AGI_VM_HALT_NEW_ROOM) {
prevRoom = vm.vars[0];
vm.vars[0] = vm.newRoomId;
vm.vars[1] = prevRoom;
// Reset most flags, then set flag 5 ("new room") to
// signal the room logic that its init branch should
// fire. In the real interpreter the engine also resets
// a handful of cycle vars (input, signal, etc.) but for
// the sub-D pipeline test the new-room flag is the only
// one room logic actually checks.
for (i = 0u; i < 32u; i++) {
vm.flags[i] = 0u;
}
vm.flags[5] = 1u;
agiVmResetToLogic(&vm, logic0, length, 0u);
continue;
}
if (reason == AGI_VM_HALT_RETURN) {
// End-of-cycle housekeeping the real interpreter would
// perform: clear flag 5 (new.room) so subsequent cycles
// don't re-run the room-init branch.
vm.flags[5] = 0u;
agiVmResetToLogic(&vm, logic0, length, 0u);
continue;
}
break;
}
printf("===== trace =====\n");
printf(" fetchLogic: %u (first-loads only)\n", (unsigned)gTraceFetchLogic);
printf(" loadPic: %u\n", (unsigned)gTraceLoadPic);
printf(" drawPic: %u\n", (unsigned)gTraceDrawPic);
printf(" showPic: %u\n", (unsigned)gTraceShowPic);
printf(" discardPic: %u\n", (unsigned)gTraceDiscardPic);
printf(" overlayPic: %u\n", (unsigned)gTraceOverlayPic);
releaseCache();
agiResClose(&gGame);
return 0;
}