Files
Joseph MattielloandClaude Fable 5 e92b6750d8 fix(cd): GPU IRQ semantics — halted GPU captures nothing; auth magic stops stomping game GPU code
Two related fixes for BIOS-mode CD titles:

1. Hover Strike B-skip lockup (deterministic at press frames
   150/200/550/650): GPUSetIRQLine latched+dispatched interrupts while
   the GPU was halted (GPUGO=0).  A stale DSA-response latch captured
   during the driver's stopped-GPU maintenance window dispatched at
   restart before the new GPU program's r31 stack init, pushing the
   return address into GPU code (the PIT stub's movei operand at
   $F03114) — the game's schedule clock at $5DC2E froze and the 68K
   polled forever.  A halted RISC samples nothing: asserts are now
   dropped while stopped (the source's level re-edges after restart),
   GPUHandleIRQs bails while halted, and the G_CTRL write handler
   applies GO before processing the CPU->GPU INT0 strobe so a single
   GO|INT0 write still works.  The BUTCH GPU latch assert is now
   edge-paced (once per service cycle, re-armed by the ISR's BUTCH+2
   ack write) instead of once per BUTCHExec tick — per-tick assert
   kept a latch pending at every instant, per-onset starves transfers,
   and edges arriving during a stopped window stay armed so the first
   tick after restart re-asserts them.

2. Myst BIOS boot crash (GPU flies to $ACC5A186 at the post-load
   Pause): the BIOS strategy's auth-magic hook wrote $03D0DEAD to
   $F03000 on EVERY 68K execution of PC=$005E40.  That address is
   BIOS-phase code, but Myst loads its game code at $5000-$1D380 and
   hot paths cross $5E40 — each crossing overwrote the first two words
   of Myst's GPU IRQ0 handler stub (`movei #$F0D000,r0 / jump (r0)`;
   Myst uses IRQ0 as its 68K->GPU command doorbell, G_CTRL=$05 kicks).
   The corrupted slot dispatched through stale r0 into garbage.  The
   hook is now gated on the boot stub not yet having been injected —
   after BIOS->game handoff the game owns that RAM.  Myst now boots
   and runs its command loop (it renders black in headless — separate
   issue, likely its CDDA-driven attract flow; the crash is gone).

Verified: Hover Strike B-press sweep 150/200/350/550/650 all clean;
Myst BIOS 1500 frames no crash signature; full suite passes (56103);
CD boot matrix 9 titles x 2 modes no regressions, Baldies HLE improved
0/1 -> 1/1.  test_hle_bios 9d/9d-cd and test_butch_cd DSA-routing test
updated to pin the new halted-GPU semantics (latch mechanics now run
with GPUGO=1; new sub-assert pins assert-drop while halted).

Tooling: cd_wedge_probe gains --ram-dump / --snap / --snap-prefix
(full main-RAM + GPU-RAM snapshots) and fixes the jaguarMainRAM
pointer-variable dereference bug that made all prior RAM dumps read
host heap instead of emulated RAM.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-18 07:17:03 -04:00

482 lines
18 KiB
C

/*
* test_butch_cd.c — BUTCH CD controller register accuracy tests.
*
* Validates all BUTCH register read/write behavior against MiSTer FPGA
* butch.v implementation. This catches CD boot regressions.
*
* Build: cc -g -O0 -o test/test_butch_cd test/test_butch_cd.c -ldl
* Run: ./test/test_butch_cd
*/
#include "test_framework.h"
#include "mister_ground_truth.h"
static struct vj_core core;
/* ================================================================== */
/* BUTCH Register Read/Write Tests */
/* ================================================================== */
TEST(butch_reset_state)
{
uint16_t hi;
uint16_t lo;
uint32_t val;
if (!core.CDROMReset) { FAIL("CDROMReset not available"); }
core.CDROMReset();
/* After reset, interrupt control should be 0 */
hi = core.CDROMReadWord(BUTCH_INT_CTRL, CALLER_M68K);
lo = core.CDROMReadWord(BUTCH_INT_CTRL + 2, CALLER_M68K);
val = ((uint32_t)hi << 16) | lo;
/* Master enable and all status bits should be clear */
CHECK_EQ(val & BUTCH_INT_ENABLE, 0);
}
TEST(butch_int_enable_write)
{
uint16_t data;
uint16_t readback;
core.CDROMReset();
/* Write master enable + FIFO enable */
data = BUTCH_INT_ENABLE | BUTCH_INT_FIFO_EN;
core.CDROMWriteWord(BUTCH_INT_CTRL + 2, data, CALLER_M68K);
/* Note: readback of BUTCH enable bits requires haveCDGoodness in the
* emulator (set when a disc is loaded). Without a disc loaded, the
* status read path is bypassed and returns 0. This is a known
* implementation detail, not a hardware behavior — MiSTer always
* returns enables in the read. Marking as CHECK for now. */
readback = core.CDROMReadWord(BUTCH_INT_CTRL + 2, CALLER_M68K);
CHECK_EQ(readback & (BUTCH_INT_ENABLE | BUTCH_INT_FIFO_EN),
BUTCH_INT_ENABLE | BUTCH_INT_FIFO_EN);
}
TEST(butch_dscntrl_enable)
{
uint16_t hi;
core.CDROMReset();
/* Write DSA enable ($10000) to DSCNTRL */
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K); /* high word: bit 16 */
hi = core.CDROMReadWord(BUTCH_DSCNTRL, CALLER_M68K);
CHECK_EQ(hi & 0x0001, 0x0001);
}
TEST(butch_i2s_ctrl_bits)
{
uint16_t i2s_val;
uint16_t readback;
core.CDROMReset();
/* Write I2S control: drive=1, jerry=1, fifo_en=1 */
i2s_val = BUTCH_I2S_DRIVE | BUTCH_I2S_JERRY | BUTCH_I2S_FIFO_EN;
core.CDROMWriteWord(BUTCH_I2CNTRL + 2, i2s_val, CALLER_M68K);
readback = core.CDROMReadWord(BUTCH_I2CNTRL + 2, CALLER_M68K);
CHECK_EQ(readback & 0x07, i2s_val & 0x07);
}
TEST(butch_subcode_ctrl_write)
{
uint16_t readback;
core.CDROMReset();
core.CDROMWriteWord(BUTCH_SBCNTRL + 2, 0x0001, CALLER_M68K);
readback = core.CDROMReadWord(BUTCH_SBCNTRL + 2, CALLER_M68K);
CHECK_EQ(readback & 0x0001, 0x0001);
}
TEST(butch_fifo_initial_empty)
{
uint16_t i2s_stat;
core.CDROMReset();
/* FIFO should be empty after reset — fifonempty bit should be 0 */
i2s_stat = core.CDROMReadWord(BUTCH_I2CNTRL + 2, CALLER_M68K);
CHECK_EQ(i2s_stat & BUTCH_I2S_FIFONEMPTY, 0);
}
TEST(butch_address_decode_range)
{
uint32_t offset;
core.CDROMReset();
/* All 12 BUTCH registers (each 4 bytes) should be accessible */
/* Write patterns to each, verify no crash */
for (offset = 0; offset <= 0x2C; offset += 4) {
uint32_t addr = BUTCH_BASE + offset;
core.CDROMWriteWord(addr, 0x0000, CALLER_M68K);
core.CDROMWriteWord(addr + 2, 0x0000, CALLER_M68K);
core.CDROMReadWord(addr, CALLER_M68K);
core.CDROMReadWord(addr + 2, CALLER_M68K);
}
ASSERT_TRUE(1); /* If we get here without crash, decode works */
}
/* ================================================================== */
/* DSA Command/Response Protocol Tests */
/* ================================================================== */
TEST(butch_dsa_command_write)
{
uint16_t cmd;
core.CDROMReset();
/* Enable DSA */
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DSCNTRL + 2, 0x0000, CALLER_M68K);
/* Write a STOP command to DS_DATA */
cmd = (DSA_CMD_STOP << 8) | 0x00;
core.CDROMWriteWord(BUTCH_DS_DATA, cmd, CALLER_M68K);
/* Should not crash — command is queued */
ASSERT_TRUE(1);
}
TEST(butch_dsa_read_toc_command)
{
uint16_t cmd;
uint16_t resp;
core.CDROMReset();
/* Enable DSA */
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DSCNTRL + 2, 0x0000, CALLER_M68K);
/* Send READ_TOC command */
cmd = (DSA_CMD_READ_TOC << 8) | 0x00;
core.CDROMWriteWord(BUTCH_DS_DATA, cmd, CALLER_M68K);
/* Read response — should get TOC data or error */
resp = core.CDROMReadWord(BUTCH_DS_DATA, CALLER_M68K);
(void)resp; /* Just verify no crash */
ASSERT_TRUE(1);
}
TEST(butch_dsa_get_status_command)
{
uint16_t cmd;
uint16_t resp;
core.CDROMReset();
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DSCNTRL + 2, 0x0000, CALLER_M68K);
cmd = (DSA_CMD_GET_STATUS << 8) | 0x00;
core.CDROMWriteWord(BUTCH_DS_DATA, cmd, CALLER_M68K);
resp = core.CDROMReadWord(BUTCH_DS_DATA, CALLER_M68K);
/* Response code should be DSA_RSP_DISC_STATUS (0x03xx) */
(void)resp;
ASSERT_TRUE(1);
}
/* ================================================================== */
/* I2S FIFO Tests */
/* ================================================================== */
TEST(butch_fifo_write_read)
{
uint16_t i2s_stat;
core.CDROMReset();
/* Enable I2S FIFO */
core.CDROMWriteWord(BUTCH_I2CNTRL + 2,
BUTCH_I2S_DRIVE | BUTCH_I2S_FIFO_EN, CALLER_M68K);
/* Write data to FIFO via I2SDAT1 */
core.CDROMWriteWord(BUTCH_I2SDAT1, 0xDEAD, CALLER_M68K);
core.CDROMWriteWord(BUTCH_I2SDAT1 + 2, 0xBEEF, CALLER_M68K);
/* FIFO should now be non-empty */
i2s_stat = core.CDROMReadWord(BUTCH_I2CNTRL + 2, CALLER_M68K);
/* Note: fifonempty behavior depends on whether writes actually push to FIFO */
(void)i2s_stat;
ASSERT_TRUE(1);
}
TEST(butch_fifo_dat1_dat2_both_read)
{
uint16_t dat1_hi;
uint16_t dat1_lo;
uint16_t dat2_hi;
uint16_t dat2_lo;
core.CDROMReset();
core.CDROMWriteWord(BUTCH_I2CNTRL + 2,
BUTCH_I2S_DRIVE | BUTCH_I2S_FIFO_EN, CALLER_M68K);
/* Per MiSTer butch.v: I2SDAT1 ($DFFF24) and I2SDAT2 ($DFFF28) both
* read from the same FIFO. They exist to allow consecutive reads
* without needing to re-address. */
dat1_hi = core.CDROMReadWord(BUTCH_I2SDAT1, CALLER_M68K);
dat1_lo = core.CDROMReadWord(BUTCH_I2SDAT1 + 2, CALLER_M68K);
dat2_hi = core.CDROMReadWord(BUTCH_I2SDAT2, CALLER_M68K);
dat2_lo = core.CDROMReadWord(BUTCH_I2SDAT2 + 2, CALLER_M68K);
(void)dat1_hi; (void)dat1_lo; (void)dat2_hi; (void)dat2_lo;
ASSERT_TRUE(1); /* Structural — verify both addresses decode */
}
/* ================================================================== */
/* EEPROM Interface Tests */
/* ================================================================== */
TEST(butch_eeprom_cs_active_low)
{
uint16_t readback;
core.CDROMReset();
/* MiSTer butch.v line 302: eeprom_cs = !butch_reg[11][0]
* So writing 0 to bit 0 = CS active (asserted)
* Writing 1 to bit 0 = CS inactive (deasserted) */
core.CDROMWriteWord(BUTCH_EEPROM + 2, 0x0000, CALLER_M68K);
/* CS should be active when bit 0 = 0 */
readback = core.CDROMReadWord(BUTCH_EEPROM + 2, CALLER_M68K);
CHECK_EQ(readback & BUTCH_EE_CS, 0); /* CS bit reads 0 = asserted */
}
/* ================================================================== */
/* Interrupt Logic Tests */
/* ================================================================== */
TEST(butch_eint_requires_master_enable)
{
uint16_t ctrl;
core.CDROMReset();
/* Set FIFO status bit (simulate half-full condition) without master enable.
* External interrupt should NOT fire. */
/* Enable FIFO interrupt but NOT master */
core.CDROMWriteWord(BUTCH_INT_CTRL + 2, BUTCH_INT_FIFO_EN, CALLER_M68K);
/* Without master enable (bit 0), no interrupt should propagate */
/* This is a structural test — verify the logic path exists */
ctrl = core.CDROMReadWord(BUTCH_INT_CTRL + 2, CALLER_M68K);
CHECK_EQ(ctrl & BUTCH_INT_ENABLE, 0);
}
TEST(butch_int_fifo_requires_both_bits)
{
uint16_t ctrl_hi;
core.CDROMReset();
/* Per MiSTer: fifo_int = butch_reg[0][9] && butch_reg[0][1]
* Both the status bit AND the enable bit must be set for interrupt.
* Enable bit alone shouldn't trigger. */
core.CDROMWriteWord(BUTCH_INT_CTRL + 2,
BUTCH_INT_ENABLE | BUTCH_INT_FIFO_EN, CALLER_M68K);
/* FIFO status (bit 9) won't be set unless FIFO is actually half-full */
ctrl_hi = core.CDROMReadWord(BUTCH_INT_CTRL, CALLER_M68K);
/* Status bit 9 should be in high word — check it's not spuriously set */
(void)ctrl_hi;
ASSERT_TRUE(1);
}
/* ================================================================== */
/* CD interrupt routing — BUTCH -> JERRY ext -> GPU IRQ1 */
/* ================================================================== */
/* The CD BIOS installs its CD-data ISR at $F03010 — the GPU IRQ **1**
* vector (JTRM: vector = int# * 16; int 1 = DSP/JERRY source) — and
* enables only G_FLAGS INT_ENA1 ($20). Its ISR epilogue acks via
* INT_CLR1 (G_FLAGS bit 10) and re-arms the JERRY external latch
* (J_INT = $0101), because BUTCH's eint enters through JERRY
* (MiSTer butch.v line 83: "external interrupt to Jerry").
*
* Therefore when a DSA response is ready and BUTCH interrupts are
* enabled, the GPU must see IRQ **1** latched (G_CTRL INT_LAT1, bit 7)
* and dispatch to $F03010. Asserting IRQ0 instead leaves the interrupt
* permanently masked (BIOS never sets INT_ENA0) — the FIFO-never-fills
* boot deadlock (Primal Rage / Highlander / Iron Soldier 2, bios mode).
*
* Needs a loaded disc image (haveCDGoodness gates BUTCHExec), so we
* synthesize a 16-sector single-track CUE/BIN — no ROM required. */
/* Back-to-back single-word DSA commands must BOTH deliver their responses.
* Device-traced on Baldies (bios, 2026-07-15): after transfer #2 the game
* writes $7001 (Set DAC Mode) and $150A (Set Mode) to DS_DATA in the same
* tick without reading between them. With a single command latch the
* $70nn echo is silently replaced by the $17nn mode status; the game polls
* DS_DATA repeatedly for its echo and wedges (cd_seek_wedge, GPU spinning).
* Real hardware queues each drive response in the DSA RX path. */
TEST(butch_i2cntrl_status_bit_readonly)
{
uint16_t readback;
core.CDROMReset();
/* Games do read-modify-write cycles on I2CNTRL, so a set FIFO-not-empty
* status bit (b4, read-only per butch.v) rides along in the write data.
* Device-traced on Dragon's Lair (bios): end-of-transfer writes $0011,
* then flushes the FIFO with a "btst #4; bne" loop. If the written b4
* is stored and echoed, the loop never sees FIFO-empty and the 68K
* wedges. With no FIFO data pending, b4 must read back 0 regardless
* of what was written. */
core.CDROMWriteWord(BUTCH_I2CNTRL + 2,
BUTCH_I2S_DRIVE | BUTCH_I2S_FIFONEMPTY, CALLER_M68K);
readback = core.CDROMReadWord(BUTCH_I2CNTRL + 2, CALLER_M68K);
CHECK_EQ(readback & BUTCH_I2S_FIFONEMPTY, 0);
/* the writable bits still stick */
CHECK_EQ(readback & BUTCH_I2S_DRIVE, BUTCH_I2S_DRIVE);
}
TEST(butch_dsa_back_to_back_responses)
{
bool (*openImage)(const char *);
void (*closeImage)(void);
char dir[512], cuePath[600], binPath[600];
static const char *tmpl = "/tmp";
const char *tmp;
FILE *f;
uint32_t i;
uint16_t r1, r2;
static uint8_t sector[2352];
openImage = (bool (*)(const char *))dlsym(core.handle, "CDIntfOpenImage");
closeImage = (void (*)(void))dlsym(core.handle, "CDIntfCloseImage");
if (!openImage || !closeImage)
{ FAIL("CDIntfOpenImage/CDIntfCloseImage not exported (need TEST_EXPORTS=1)"); }
tmp = getenv("TMPDIR");
if (!tmp) tmp = tmpl;
snprintf(dir, sizeof(dir), "%s", tmp);
snprintf(binPath, sizeof(binPath), "%s/t7_dsa_b2b.bin", dir);
snprintf(cuePath, sizeof(cuePath), "%s/t7_dsa_b2b.cue", dir);
f = fopen(binPath, "wb");
if (!f) { FAIL("cannot create synthetic BIN"); }
for (i = 0; i < 16; i++)
fwrite(sector, 1, sizeof(sector), f);
fclose(f);
f = fopen(cuePath, "w");
if (!f) { remove(binPath); FAIL("cannot create synthetic CUE"); }
fprintf(f, "FILE \"t7_dsa_b2b.bin\" BINARY\n"
" TRACK 01 MODE1/2352\n"
" INDEX 01 00:00:00\n");
fclose(f);
if (!openImage(cuePath))
{ remove(cuePath); remove(binPath); FAIL("synthetic CUE did not load"); }
core.CDROMInit();
core.CDROMReset();
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K);
/* The traced sequence: two commands, no read in between */
core.CDROMWriteWord(BUTCH_DS_DATA, 0x7001, CALLER_M68K); /* Set DAC Mode 1 */
core.CDROMWriteWord(BUTCH_DS_DATA, 0x150A, CALLER_M68K); /* Set Mode $0A */
r1 = core.CDROMReadWord(BUTCH_DS_DATA, CALLER_M68K);
r2 = core.CDROMReadWord(BUTCH_DS_DATA, CALLER_M68K);
closeImage();
remove(cuePath);
remove(binPath);
CHECK_EQ(r1, 0x7001); /* DAC-mode echo must not be lost */
CHECK_EQ(r2, 0x170A); /* then the mode status */
}
TEST(butch_dsa_irq_routes_to_gpu_irq1)
{
bool (*openImage)(const char *);
void (*closeImage)(void);
void (*butchExec)(uint32_t);
char dir[512], cuePath[600], binPath[600];
static const char *tmpl = "/tmp";
const char *tmp;
FILE *f;
uint32_t i, gctrl, gpc;
static uint8_t sector[2352];
openImage = (bool (*)(const char *))dlsym(core.handle, "CDIntfOpenImage");
closeImage = (void (*)(void))dlsym(core.handle, "CDIntfCloseImage");
butchExec = (void (*)(uint32_t))dlsym(core.handle, "BUTCHExec");
if (!openImage || !closeImage || !butchExec)
{ FAIL("CDIntfOpenImage/CDIntfCloseImage/BUTCHExec not exported (need TEST_EXPORTS=1)"); }
tmp = getenv("TMPDIR");
if (!tmp) tmp = tmpl;
snprintf(dir, sizeof(dir), "%s", tmp);
snprintf(binPath, sizeof(binPath), "%s/t6_butch_irq.bin", dir);
snprintf(cuePath, sizeof(cuePath), "%s/t6_butch_irq.cue", dir);
f = fopen(binPath, "wb");
if (!f) { FAIL("cannot create synthetic BIN"); }
for (i = 0; i < 16; i++)
fwrite(sector, 1, sizeof(sector), f);
fclose(f);
f = fopen(cuePath, "w");
if (!f) { remove(binPath); FAIL("cannot create synthetic CUE"); }
fprintf(f, "FILE \"t6_butch_irq.bin\" BINARY\n"
" TRACK 01 MODE1/2352\n"
" INDEX 01 00:00:00\n");
fclose(f);
if (!openImage(cuePath))
{ remove(cuePath); remove(binPath); FAIL("synthetic CUE did not load"); }
core.CDROMInit(); /* re-latch haveCDGoodness now that an image is open */
core.CDROMReset();
core.GPUReset();
/* Mimic the CD BIOS interrupt setup exactly:
* G_FLAGS = INT_ENA1 only; BUTCH = master enable + DSA RX enable.
* GPUGO must be set: the BIOS's GPU is RUNNING (parked in its wait
* loop) when the DSA response arrives — a halted GPU captures no
* interrupts at all (asserts are dropped, not latched; that stale
* latch dispatching at restart was Hover Strike's B-skip lockup).
* INT_ENA1 + a pending latch dispatches immediately on assert, so
* the "running" GPU never needs GPUExec to run garbage code. */
core.GPUWriteLong(0xF02114, 0x00000001, CALLER_M68K);
core.GPUWriteLong(0xF02100, GPU_FLAGS_INT_ENA1, CALLER_M68K);
core.CDROMWriteWord(BUTCH_INT_CTRL + 2,
BUTCH_INT_ENABLE | BUTCH_INT_RBUF_EN, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DSCNTRL, 0x0001, CALLER_M68K); /* DSA enable */
/* Seek to MSF 00:02:00 (block 0) — Goto Min / Sec / Frame. */
core.CDROMWriteWord(BUTCH_DS_DATA, 0x1000, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DS_DATA, 0x1102, CALLER_M68K);
core.CDROMWriteWord(BUTCH_DS_DATA, 0x1200, CALLER_M68K);
/* Tick BUTCH past SEEK_DELAY_TICKS so the $0100 response queues and
* the DSA interrupt fires. */
for (i = 0; i < 200; i++)
butchExec(0);
gctrl = core.GPUReadLong(0xF02114, CALLER_M68K);
gpc = core.GPUReadLong(0xF02110, CALLER_M68K);
closeImage();
remove(cuePath);
remove(binPath);
/* INT_LAT1 (bit 7) must be latched — the line the BIOS enables. */
CHECK_EQ((gctrl >> 7) & 1, 1);
/* And with only INT_ENA1 set, the GPU must dispatch to the IRQ1
* vector, where the BIOS's CD ISR entry stub lives. */
CHECK_EQ(gpc, 0xF03010);
}
/* ================================================================== */
/* Main */
/* ================================================================== */
int main(int argc, char *argv[])
{
(void)argc; (void)argv;
TEST_INIT("BUTCH CD Controller Accuracy");
if (!vj_core_load(&core)) return 1;
vj_core_init(&core);
/* Register read/write */
RUN_TEST(butch_reset_state);
RUN_TEST(butch_int_enable_write);
RUN_TEST(butch_dscntrl_enable);
RUN_TEST(butch_i2s_ctrl_bits);
RUN_TEST(butch_subcode_ctrl_write);
RUN_TEST(butch_fifo_initial_empty);
RUN_TEST(butch_address_decode_range);
/* DSA command/response */
RUN_TEST(butch_dsa_command_write);
RUN_TEST(butch_dsa_read_toc_command);
RUN_TEST(butch_dsa_get_status_command);
RUN_TEST(butch_i2cntrl_status_bit_readonly);
RUN_TEST(butch_dsa_back_to_back_responses);
/* FIFO */
RUN_TEST(butch_fifo_write_read);
RUN_TEST(butch_fifo_dat1_dat2_both_read);
/* EEPROM */
RUN_TEST(butch_eeprom_cs_active_low);
/* Interrupt logic */
RUN_TEST(butch_eint_requires_master_enable);
RUN_TEST(butch_int_fifo_requires_both_bits);
RUN_TEST(butch_dsa_irq_routes_to_gpu_irq1);
vj_core_unload(&core);
return TEST_REPORT();
}