284 lines
8.7 KiB
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;
|
|
}
|