Files
Joseph MattielloandClaude Opus 5.5 4e0bd1cfb4 fix(risc): DIV uses the netlist's 33-bit working value (#895)
jag_sim DIVIDE.NET (TOM and JERRY identical): an unsigned non-restoring
divider whose working value is the 32-bit dividend_hi register plus the
last_neg sign flop -- both adders carry a 33rd bit "to correctly
determine the sign, as the arguments are 32-bit unsigned values".  The
core's loop kept a 32-bit working value and took the sign from bit 31,
so every divisor above $80000000 gave garbage ($FCC00000/$FCC00000 =
$F9A9342A instead of 1, found in Xenowings' joypad reader), as did
16.16 divides whose quotient overflows.  For divisors up to $80000000
with an in-range quotient the result is bit-identical to before
(checked over 20M random cases).

- src/tom/risc_div.h: risc_divide(), the netlist algorithm, header-only
  like dsp_acc40.h.  Used by gpu_opcode_div, dsp_opcode_div and the
  dead pipelined DSP_div (DSPExecP is never defined), so no two paths
  can disagree.
- The DSP's divide-by-zero guard goes: the hardware has no special case,
  every step subtracts 0, giving $FFFFFFFF (unchanged) with the dividend
  in D_REMAIN (was 0).
- Tests: test_dsp_unit DIV cases incl. remainders (an even quotient ends
  on a negative step: remainder - divisor, as DIVIDE.NET documents);
  test_gpu_ops $FCC00000/$FCC00000.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 19:43:14 -04:00

1535 lines
52 KiB
C

/* test_dsp_unit.c -- Comprehensive DSP register and interrupt unit tests.
* Loads the core via dlopen, creates a minimal dummy ROM, then tests DSP
* control/flags register behavior, interrupt dispatch ordering, register
* banking, and CINT/INT_LAT interactions.
*
* Build: cc -o test/test_dsp_unit test/test_dsp_unit.c -ldl
* Usage: ./test/test_dsp_unit
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <stdarg.h>
#include <stdbool.h>
#include "../libretro-common/include/libretro.h"
#ifdef __APPLE__
#define CORE_FILENAME "virtualjaguar_libretro.dylib"
#elif defined(_WIN32)
#define CORE_FILENAME "virtualjaguar_libretro.dll"
#else
#define CORE_FILENAME "virtualjaguar_libretro.so"
#endif
/* DSP register addresses (memory-mapped) */
#define DSP_FLAGS_ADDR 0xF1A100
#define DSP_MCTRL_ADDR 0xF1A104
#define DSP_MBASE_ADDR 0xF1A108
#define DSP_DORG_ADDR 0xF1A10C
#define DSP_PC_ADDR 0xF1A110
#define DSP_CTRL_ADDR 0xF1A114
#define DSP_MOD_ADDR 0xF1A118
#define DSP_DIVCTRL_ADDR 0xF1A11C
#define DSP_RAM_BASE 0xF1B000
/* DSP FLAGS bits */
#define ZERO_FLAG 0x00001
#define CARRY_FLAG 0x00002
#define NEGA_FLAG 0x00004
#define IMASK 0x00008
#define INT_ENA0 0x00010
#define INT_ENA1 0x00020
#define INT_ENA2 0x00040
#define INT_ENA3 0x00080
#define INT_ENA4 0x00100
#define CINT0FLAG 0x00200
#define CINT1FLAG 0x00400
#define CINT2FLAG 0x00800
#define CINT3FLAG 0x01000
#define CINT4FLAG 0x02000
#define REGPAGE 0x04000
#define DMAEN 0x08000
#define INT_ENA5 0x10000
#define CINT5FLAG 0x20000
/* DSP CTRL bits */
#define DSPGO 0x00001
#define CPUINT 0x00002
#define DSPINT0 0x00004
#define SINGLE_STEP 0x00008
#define SINGLE_GO 0x00010
#define INT_LAT0 0x00040
#define INT_LAT1 0x00080
#define INT_LAT2 0x00100
#define INT_LAT3 0x00200
#define INT_LAT4 0x00400
#define BUS_HOG 0x00800
#define VERSION_MASK 0x0F000
#define INT_LAT5 0x10000
/* DSP opcodes */
#define DSP_OP(opc, r1, r2) ((uint16_t)(((opc) << 10) | ((r1) << 5) | (r2)))
#define OP_NOP DSP_OP(57, 0, 0)
#define OP_MOVEQ(n, rd) DSP_OP(35, (n), (rd))
#define OP_MOVE(rs, rd) DSP_OP(34, (rs), (rd))
#define OP_STORE(rs, rd) DSP_OP(47, (rs), (rd))
#define OP_LOAD(rs, rd) DSP_OP(41, (rs), (rd))
#define OP_ADD(rs, rd) DSP_OP(0, (rs), (rd))
#define OP_JR(cc, off) DSP_OP(53, (off) & 0x1F, (cc))
#define OP_JUMP(cc, rm) DSP_OP(52, (rm), (cc))
/* libretro function pointers */
static void (*p_retro_init)(void);
static void (*p_retro_deinit)(void);
static void (*p_retro_set_environment)(retro_environment_t);
static void (*p_retro_set_video_refresh)(retro_video_refresh_t);
static void (*p_retro_set_audio_sample)(retro_audio_sample_t);
static void (*p_retro_set_audio_sample_batch)(retro_audio_sample_batch_t);
static void (*p_retro_set_input_poll)(retro_input_poll_t);
static void (*p_retro_set_input_state)(retro_input_state_t);
static bool (*p_retro_load_game)(const struct retro_game_info *);
static void (*p_retro_unload_game)(void);
static void (*p_retro_run)(void);
/* Emulator internals via dlsym */
static void *core_handle;
static uint32_t *p_dsp_control;
static uint32_t *p_dsp_pc;
static uint32_t *p_dsp_reg_bank_0;
static uint32_t *p_dsp_reg_bank_1;
static uint8_t *(*p_DSPGetRAM)(void);
static void (*p_DSPWriteLong)(uint32_t, uint32_t, uint32_t);
static uint32_t (*p_DSPReadLong)(uint32_t, uint32_t);
static void (*p_DSPReset)(void);
static void (*p_DSPExec)(int32_t);
static void (*p_DSPSetIRQLine)(int, int);
static bool (*p_DSPIsRunning)(void);
static void (*p_DSPInit)(void);
static size_t (*p_DSPStateSave)(uint8_t *);
static size_t (*p_DSPStateLoad)(const uint8_t *);
/* Stub callbacks */
static void video_refresh(const void *d, unsigned w, unsigned h, size_t p)
{ (void)d; (void)w; (void)h; (void)p; }
static void audio_sample(int16_t l, int16_t r) { (void)l; (void)r; }
static size_t audio_batch(const int16_t *d, size_t f) { (void)d; return f; }
static void input_poll(void) {}
static int16_t input_state(unsigned p, unsigned d, unsigned i, unsigned id)
{ (void)p; (void)d; (void)i; (void)id; return 0; }
static void log_printf(enum retro_log_level level, const char *fmt, ...)
{
va_list ap;
if (level < RETRO_LOG_WARN) return;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
static struct retro_log_callback log_cb = { log_printf };
static bool environment(unsigned cmd, void *data)
{
switch (cmd) {
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
*(struct retro_log_callback *)data = log_cb;
return true;
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
case RETRO_ENVIRONMENT_SET_VARIABLES:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2:
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
case RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS:
case RETRO_ENVIRONMENT_SET_GEOMETRY:
case RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION:
case RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER:
return true;
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
*(const char **)data = "/tmp";
return true;
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
*(const char **)data = "/tmp";
return true;
case RETRO_ENVIRONMENT_GET_VARIABLE: {
struct retro_variable *var = (struct retro_variable *)data;
if (var->key && strcmp(var->key, "virtualjaguar_bios") == 0) {
var->value = "disabled";
return true;
}
if (var->key && strcmp(var->key, "virtualjaguar_usefastblitter") == 0) {
var->value = "enabled";
return true;
}
var->value = NULL;
return false;
}
default:
return false;
}
}
/* Test counters */
static int passes = 0, fails = 0;
#define PASS(msg, ...) do { printf(" PASS: " msg "\n", ##__VA_ARGS__); passes++; } while(0)
#define FAIL(msg, ...) do { printf(" FAIL: " msg "\n", ##__VA_ARGS__); fails++; } while(0)
/* Write a 16-bit value to DSP RAM at a given byte offset from DSP_RAM_BASE */
static void write_dsp_ram16(uint16_t offset, uint16_t val)
{
uint8_t *ram = p_DSPGetRAM();
ram[offset] = (val >> 8) & 0xFF;
ram[offset + 1] = val & 0xFF;
}
/* Write a 32-bit value to DSP RAM at a given byte offset from DSP_RAM_BASE */
static void write_dsp_ram32(uint16_t offset, uint32_t val)
{
uint8_t *ram = p_DSPGetRAM();
ram[offset] = (val >> 24) & 0xFF;
ram[offset + 1] = (val >> 16) & 0xFF;
ram[offset + 2] = (val >> 8) & 0xFF;
ram[offset + 3] = val & 0xFF;
}
/* Read a 32-bit value from DSP RAM */
static uint32_t read_dsp_ram32(uint16_t offset)
{
uint8_t *ram = p_DSPGetRAM();
return ((uint32_t)ram[offset] << 24)
| ((uint32_t)ram[offset + 1] << 16)
| ((uint32_t)ram[offset + 2] << 8)
| (uint32_t)ram[offset + 3];
}
/* Write a movei instruction (48-bit: opcode + lo16 + hi16) */
static void write_movei(uint16_t ram_offset, uint32_t imm, uint8_t rd)
{
uint16_t op = DSP_OP(38, 0, rd);
uint16_t lo = imm & 0xFFFF;
uint16_t hi = (imm >> 16) & 0xFFFF;
write_dsp_ram16(ram_offset, op);
write_dsp_ram16(ram_offset + 2, lo);
write_dsp_ram16(ram_offset + 4, hi);
}
/* ================================================================
* Test 1: DSP Reset State
* Verify DSP comes up in expected initial state
* ================================================================ */
static void test_dsp_reset_state(void)
{
uint32_t ctrl, pc;
printf("\n=== Test 1: DSP Reset State ===\n");
p_DSPReset();
ctrl = *p_dsp_control;
pc = *p_dsp_pc;
if ((ctrl & VERSION_MASK) == 0x2000)
PASS("DSP reports version 2");
else
FAIL("DSP version wrong: ctrl=%08X (expected VERSION=2)", ctrl);
if (!(ctrl & DSPGO))
PASS("DSP is stopped after reset");
else
FAIL("DSP is running after reset: ctrl=%08X", ctrl);
if (pc == 0x00F1B000)
PASS("DSP PC is F1B000 after reset");
else
FAIL("DSP PC wrong after reset: %08X (expected F1B000)", pc);
if (!(ctrl & (INT_LAT0|INT_LAT1|INT_LAT2|INT_LAT3|INT_LAT4|INT_LAT5)))
PASS("No interrupt latches set after reset");
else
FAIL("Interrupt latches set after reset: ctrl=%08X", ctrl);
}
/* ================================================================
* Test 2: DSPGO Start/Stop
* Verify DSPGO bit controls DSP execution state
* ================================================================ */
static void test_dspgo(void)
{
uint32_t off;
printf("\n=== Test 2: DSPGO Start/Stop ===\n");
p_DSPReset();
/* Fill DSP RAM with NOPs so we can safely start it */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Write DSPGO to start the DSP */
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
if (*p_dsp_control & DSPGO)
PASS("DSPGO bit set after write");
else
FAIL("DSPGO bit not set: ctrl=%08X", *p_dsp_control);
if (p_DSPIsRunning())
PASS("DSPIsRunning() returns true");
else
FAIL("DSPIsRunning() returns false after DSPGO");
/* Clear DSPGO to stop */
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
if (!(*p_dsp_control & DSPGO))
PASS("DSPGO cleared after stop");
else
FAIL("DSPGO still set: ctrl=%08X", *p_dsp_control);
}
/* ================================================================
* Test 3: INT_LAT Protection
* Verify external writes cannot clear INT_LAT bits via ctrl register
* ================================================================ */
static void test_int_lat_protection(void)
{
printf("\n=== Test 3: INT_LAT Protection ===\n");
p_DSPReset();
/* Set INT_LAT0 via DSPSetIRQLine */
p_DSPSetIRQLine(0, 1);
if (*p_dsp_control & INT_LAT0)
PASS("INT_LAT0 set by DSPSetIRQLine");
else
FAIL("INT_LAT0 not set: ctrl=%08X", *p_dsp_control);
/* Try to clear INT_LAT0 by writing 0 to ctrl -- should be protected */
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
if (*p_dsp_control & INT_LAT0)
PASS("INT_LAT0 protected from external write");
else
FAIL("INT_LAT0 was cleared by ctrl write: ctrl=%08X", *p_dsp_control);
}
/* ================================================================
* Test 4: CINT Clears INT_LAT
* Writing CINTn to flags register should clear corresponding INT_LATn
* ================================================================ */
static void test_cint_clears_lat(void)
{
printf("\n=== Test 4: CINT Clears INT_LAT ===\n");
p_DSPReset();
/* Set INT_LAT0 */
p_DSPSetIRQLine(0, 1);
if (!(*p_dsp_control & INT_LAT0)) {
FAIL("Setup: INT_LAT0 not set");
return;
}
/* Write CINT0 to flags -- should clear INT_LAT0 */
p_DSPWriteLong(DSP_FLAGS_ADDR, CINT0FLAG, 6);
if (!(*p_dsp_control & INT_LAT0))
PASS("CINT0 cleared INT_LAT0");
else
FAIL("CINT0 did not clear INT_LAT0: ctrl=%08X", *p_dsp_control);
/* Test CINT1 clearing INT_LAT1 */
p_DSPReset();
p_DSPSetIRQLine(1, 1);
if (!(*p_dsp_control & INT_LAT1)) {
FAIL("Setup: INT_LAT1 not set");
return;
}
p_DSPWriteLong(DSP_FLAGS_ADDR, CINT1FLAG, 6);
if (!(*p_dsp_control & INT_LAT1))
PASS("CINT1 cleared INT_LAT1");
else
FAIL("CINT1 did not clear INT_LAT1: ctrl=%08X", *p_dsp_control);
}
/* ================================================================
* Test 5: IMASK Write Protection
* Writing 1 to IMASK bit should have no effect (hardware-only)
* ================================================================ */
static void test_imask_write_protection(void)
{
uint32_t flags_read;
printf("\n=== Test 5: IMASK Write Protection ===\n");
p_DSPReset();
/* Try to set IMASK by writing it */
p_DSPWriteLong(DSP_FLAGS_ADDR, IMASK, 6);
/* Read flags back -- IMASK should NOT be set */
flags_read = p_DSPReadLong(DSP_FLAGS_ADDR, 6);
if (!(flags_read & IMASK))
PASS("IMASK cannot be set by software write");
else
FAIL("IMASK was set by software write: flags=%08X", flags_read);
}
/* ================================================================
* Test 6: Register Banking via REGPAGE
* Writing REGPAGE to flags should switch active register bank
* ================================================================ */
static void test_register_banking(void)
{
printf("\n=== Test 6: Register Banking ===\n");
p_DSPReset();
/* Initialize bank 0 registers with known values */
p_dsp_reg_bank_0[0] = 0xAAAA0000;
p_dsp_reg_bank_0[1] = 0xAAAA0001;
p_dsp_reg_bank_1[0] = 0xBBBB0000;
p_dsp_reg_bank_1[1] = 0xBBBB0001;
/* Initially in bank 0 (REGPAGE=0) */
p_DSPWriteLong(DSP_FLAGS_ADDR, 0, 6);
/* Switch to bank 1 by setting REGPAGE */
p_DSPWriteLong(DSP_FLAGS_ADDR, REGPAGE, 6);
/* Now set IMASK (via internal mechanism) and check bank forced to 0 */
/* We can't set IMASK directly, but we can verify REGPAGE works
* by checking that bank 1 is now active.
* Since we can't directly read which bank is active without running code,
* we verify through the register bank arrays. */
/* Write REGPAGE=0 (bank 0) */
p_DSPWriteLong(DSP_FLAGS_ADDR, 0, 6);
PASS("REGPAGE=0 accepted (bank 0)");
/* Write REGPAGE=1 (bank 1) */
p_DSPWriteLong(DSP_FLAGS_ADDR, REGPAGE, 6);
PASS("REGPAGE=1 accepted (bank 1)");
}
/* ================================================================
* Test 7: Interrupt Dispatch — Enable First, Then Assert IRQ
* The normal hardware flow: DSP program enables INT_ENA, then
* hardware asserts INT_LAT via DSPSetIRQLine, which dispatches.
* ================================================================ */
static void test_int_ena_dispatch(void)
{
uint32_t off;
uint32_t pc_before, pc_after;
printf("\n=== Test 7: Interrupt Dispatch (Enable Then Assert) ===\n");
#if !defined(__APPLE__)
/* TODO: this test passes on macOS Clang but fails on Linux GCC/Clang
* (PC stays at $F1B100, dispatch never lands). Same DSPHandleIRQsNP
* code path; suspect a GCC optimizer interaction with the dlsym'd
* static state. Skip on non-Apple until it can be debugged on Linux.
* Tracking under PR #119. */
printf(" SKIP: dispatch test pending Linux/GCC investigation\n");
return;
#endif
p_DSPReset();
/* Fill DSP RAM with NOPs */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Write a recognizable instruction at interrupt vector 0 ($F1B000) */
write_dsp_ram16(0x0000, OP_MOVEQ(7, 0));
write_dsp_ram16(0x0002, OP_NOP);
/* Set DSP PC to some other address and start it */
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
/* Start the DSP with INT_ENA0 already enabled */
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPWriteLong(DSP_FLAGS_ADDR, INT_ENA0, 2);
/* That D_FLAGS write is DSP-issued (who=2), so it has a two-slot
* retire window (DSP_FLAGS_RETIRE_DELAY) during which the DSP does
* not take interrupts; run it out first. */
p_DSPExec(2);
pc_before = *p_dsp_pc;
p_dsp_reg_bank_0[0] = 0;
/* Assert the IRQ line. This only latches (#744): the DSP takes the
* interrupt itself at its next instruction boundary, so an event
* callback never moves the PC on the DSP's behalf. */
p_DSPSetIRQLine(0, 1);
if (*p_dsp_pc == pc_before)
PASS("IRQ assert latched without moving PC (%08X)", pc_before);
else
FAIL("IRQ assert vectored synchronously: PC %08X -> %08X", pc_before, *p_dsp_pc);
/* One DSP step: take the interrupt, then run the handler's first
* instruction (moveq #7,r0 at vector 0). */
p_DSPExec(1);
pc_after = *p_dsp_pc;
if (pc_after == DSP_RAM_BASE + 2 && p_dsp_reg_bank_0[0] == 7)
PASS("INT_ENA0 + IRQ assert dispatched to vector 0 ($F1B000) at the next instruction");
else
FAIL("INT_ENA0 + IRQ assert did NOT dispatch: PC=%08X R0=%08X", pc_after, p_dsp_reg_bank_0[0]);
/* Stop DSP */
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* Test 8: CINT Before Dispatch Prevents Re-entrant IRQ
* When ISR writes CINT (to clear its own latch) and INT_ENA,
* the cleared latch should NOT cause a re-entrant dispatch.
* ================================================================ */
static void test_cint_before_dispatch(void)
{
uint32_t off;
printf("\n=== Test 8: CINT Before Dispatch (No Re-entrant IRQ) ===\n");
p_DSPReset();
/* Fill DSP RAM with NOPs */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Set up: INT_LAT0 pending */
p_DSPSetIRQLine(0, 1);
/* Write CINT0 + INT_ENA0 together to flags
* CINT0 should clear INT_LAT0 first, then dispatch check should
* find no pending interrupts and NOT dispatch. */
p_DSPWriteLong(DSP_FLAGS_ADDR, CINT0FLAG | INT_ENA0, 6);
if (!(*p_dsp_control & INT_LAT0))
PASS("CINT0 cleared INT_LAT0 before dispatch check");
else
FAIL("INT_LAT0 still set after CINT0: ctrl=%08X", *p_dsp_control);
}
/* ================================================================
* Test 9: Multiple Interrupt Priority
* When multiple INT_LAT bits are set, highest-numbered fires.
* Test flow: enable INT_ENA, start DSP, assert both IRQ lines.
* ================================================================ */
static void test_interrupt_priority(void)
{
uint32_t off;
uint32_t pc_after;
printf("\n=== Test 9: Interrupt Priority (Highest Wins) ===\n");
#if !defined(__APPLE__)
/* TODO: dispatch passes on macOS Clang, fails on Linux GCC/Clang
* (PC stays at $F1B800). Same root cause as Test 7. Skip on non-
* Apple until Linux debug access is available. */
printf(" SKIP: dispatch test pending Linux/GCC investigation\n");
return;
#endif
p_DSPReset();
/* Fill DSP RAM with NOPs */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Set INT_LAT0 before enabling interrupts — no dispatch yet */
p_DSPSetIRQLine(0, 1);
/* Start DSP at a neutral address with both INT_ENA enabled */
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B800, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPWriteLong(DSP_FLAGS_ADDR, INT_ENA0 | INT_ENA1, 2);
/* That D_FLAGS write is DSP-issued (who=2), so it has a two-slot
* retire window (DSP_FLAGS_RETIRE_DELAY) during which the DSP does
* not take interrupts; run it out first. */
p_DSPExec(2);
/* Assert IRQ1 — DSPSetIRQLine calls DSPHandleIRQsNP which now sees
* both INT_LAT0+INT_ENA0 and INT_LAT1+INT_ENA1 pending.
* The higher-priority one (IRQ1) should win. */
p_DSPSetIRQLine(1, 1);
/* Taken at the DSP's next instruction boundary (#744); the one
* step also runs the handler's first instruction (a NOP). */
p_DSPExec(1);
pc_after = *p_dsp_pc;
/* Vector 1 = $F1B010, Vector 0 = $F1B000 */
if (pc_after == DSP_RAM_BASE + 0x12)
PASS("Highest-priority interrupt (IRQ1 -> $F1B010) dispatched");
else if (pc_after == DSP_RAM_BASE + 0x02)
FAIL("Lower-priority interrupt (IRQ0 -> $F1B000) dispatched instead of IRQ1, PC=%08X", pc_after);
else
FAIL("Unexpected PC after dual-interrupt dispatch: %08X", pc_after);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* Test 9a: Interrupt Return Address
* IRQ dispatch pushes the interrupted PC to R31 and vectors through R30.
* This covers the live non-pipelined handler before changing semantics.
* ================================================================ */
static void test_interrupt_return_address(void)
{
uint32_t off;
uint32_t saved_pc;
printf("\n=== Test 9a: Interrupt Return Address ===\n");
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
p_dsp_reg_bank_0[31] = DSP_RAM_BASE + 0x900;
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(1);
p_DSPWriteLong(DSP_FLAGS_ADDR, INT_ENA0, 2);
/* That D_FLAGS write is DSP-issued (who=2), so it has a two-slot
* retire window (DSP_FLAGS_RETIRE_DELAY) during which the DSP does
* not take interrupts; run it out first. */
p_DSPExec(2);
p_DSPSetIRQLine(0, 1);
/* Asserting the line only latches (#744); the PC must not move until
* the DSP itself takes the interrupt. Ungated, unlike Test 7, so CI
* on every platform guards this contract. */
if (*p_dsp_pc == DSP_RAM_BASE + 0x106)
PASS("IRQ assert latched without moving PC");
else
FAIL("IRQ assert moved PC to $%08X before the DSP ran (expected $%08X)",
*p_dsp_pc, DSP_RAM_BASE + 0x106);
/* Taken at the DSP's next instruction boundary; the one step also
* runs the handler's first instruction (a NOP). */
p_DSPExec(1);
saved_pc = read_dsp_ram32(0x8FC);
if (p_dsp_reg_bank_0[31] == DSP_RAM_BASE + 0x8FC)
PASS("IRQ decremented R31 before pushing return PC");
else
FAIL("IRQ stack pointer = $%08X (expected $%08X)",
p_dsp_reg_bank_0[31], DSP_RAM_BASE + 0x8FC);
/* Last instruction executed before the interrupt: the second of the
* two retire-window NOPs ($F1B102, $F1B104). */
if (saved_pc == DSP_RAM_BASE + 0x104)
PASS("IRQ saved return PC $%08X", saved_pc);
else
FAIL("IRQ saved return PC $%08X (expected $%08X)",
saved_pc, DSP_RAM_BASE + 0x104);
if (*p_dsp_pc == DSP_RAM_BASE + 2)
PASS("IRQ vectored PC to $%08X and ran the handler's first instruction", DSP_RAM_BASE);
else
FAIL("IRQ PC = $%08X (expected $%08X)", *p_dsp_pc, DSP_RAM_BASE + 2);
if (*p_dsp_control & INT_LAT0)
PASS("IRQ latch remains set for handler acknowledgement");
else
FAIL("IRQ latch unexpectedly clear: ctrl=$%08X", *p_dsp_control);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* Test 10: DSP Code Execution (NOP Sled)
* Start DSP, run NOPs, verify PC advances
* ================================================================ */
static void test_dsp_execution(void)
{
uint32_t off;
uint32_t pc_before, pc_after;
printf("\n=== Test 10: DSP Code Execution ===\n");
p_DSPReset();
/* Fill DSP RAM with NOPs */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Set PC to $F1B100 */
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
pc_before = *p_dsp_pc;
/* Start DSP */
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
/* Run DSP for some cycles */
p_DSPExec(20);
pc_after = *p_dsp_pc;
if (pc_after > pc_before)
PASS("DSP PC advanced from %08X to %08X (+%u bytes)",
pc_before, pc_after, pc_after - pc_before);
else
FAIL("DSP PC did not advance: before=%08X after=%08X",
pc_before, pc_after);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* Test 11: MOVEQ Instruction
* Run moveq to load a register, verify via register bank
* ================================================================ */
static void test_moveq(void)
{
uint32_t off;
printf("\n=== Test 11: MOVEQ Instruction ===\n");
p_DSPReset();
/* Fill with NOPs first */
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Write: moveq #5, R0 at $F1B100 */
write_dsp_ram16(0x100, OP_MOVEQ(5, 0));
/* Then moveq #10, R1 */
write_dsp_ram16(0x102, OP_MOVEQ(10, 1));
/* Then NOPs */
/* Clear registers */
p_dsp_reg_bank_0[0] = 0;
p_dsp_reg_bank_0[1] = 0;
/* Set PC to $F1B100 and start */
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
/* Run enough cycles for 2 instructions */
p_DSPExec(10);
/* Check registers */
if (p_dsp_reg_bank_0[0] == 5)
PASS("R0 = 5 after moveq #5, R0");
else
FAIL("R0 = %u after moveq #5, R0 (expected 5)", p_dsp_reg_bank_0[0]);
if (p_dsp_reg_bank_1[1] == 10 || p_dsp_reg_bank_0[1] == 10)
PASS("R1 = 10 after moveq #10, R1");
else
FAIL("R1 = %u/%u after moveq #10, R1 (expected 10)",
p_dsp_reg_bank_0[1], p_dsp_reg_bank_1[1]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* DIV is a 33-bit non-restoring divider (jag_sim DIVIDE.NET, issue #895;
* docs/jtrm-errata.md B26): exact for divisors above $80000000, where the
* old 32-bit loop took the sign from the wrong bit. D_REMAIN reads the
* working value; a zero divisor yields $FFFFFFFF, remainder = dividend. */
static void div_case(uint32_t dividend, uint32_t divisor, uint32_t ctrl,
uint32_t want_q, uint32_t want_r)
{
uint32_t off, q, r;
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
write_dsp_ram16(0x100, DSP_OP(21, 1, 2)); /* div r1,r2 */
p_dsp_reg_bank_0[1] = divisor;
p_dsp_reg_bank_0[2] = dividend;
p_DSPWriteLong(DSP_DIVCTRL_ADDR, ctrl, 6);
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(40);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
q = p_dsp_reg_bank_0[2];
r = p_DSPReadLong(DSP_DIVCTRL_ADDR, 6);
p_DSPWriteLong(DSP_DIVCTRL_ADDR, 0, 6);
if (q == want_q && r == want_r)
PASS("div %08X/%08X (offset %u) = %08X rem %08X", dividend, divisor,
ctrl & 1, q, r);
else
FAIL("div %08X/%08X (offset %u) = %08X rem %08X, expected %08X rem %08X",
dividend, divisor, ctrl & 1, q, r, want_q, want_r);
}
static void test_div_33bit(void)
{
printf("\n=== Test: DIV 33-bit working value (#895) ===\n");
/* An even quotient ends on a negative step: remainder - divisor. */
div_case(300, 3, 0, 100, 0xFFFFFFFDu);
div_case(0xFCC00000u, 0xFCC00000u, 0, 1, 0); /* old: $F9A9342A */
div_case(0xFFFFFFFFu, 0x80000001u, 0, 1, 0x7FFFFFFEu);
div_case(0x7FFFFFFFu, 0x80000000u, 0, 0, 0x7FFFFFFFu - 0x80000000u);
div_case(0x00030000u, 0x00018000u, 1, 0x00020000u, 0xFFFE8000u); /* 3.0/1.5 (16.16) */
div_case(1234, 0, 0, 0xFFFFFFFFu, 1234);
}
/* ================================================================
* Test 12: STORE/LOAD Round Trip
* Write a value to DSP RAM via STORE, read it back via LOAD
* ================================================================ */
static void test_store_load(void)
{
uint32_t off;
uint32_t stored_val;
printf("\n=== Test 12: STORE/LOAD Round Trip ===\n");
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Program at $F1B100:
* movei #$F1B900, R10 ; target address
* moveq #31, R0 ; value to store
* store R0, (R10) ; store R0 to target
* NOP sled ; padding
*/
write_movei(0x100, 0xF1B900, 10); /* 6 bytes */
write_dsp_ram16(0x106, OP_MOVEQ(31, 0));
/* STORE: reg1=address reg, reg2=value reg. STORE Rn,(Rm) => RM=addr, RN=val */
write_dsp_ram16(0x108, OP_STORE(10, 0));
/* NOPs follow */
/* Clear target */
write_dsp_ram32(0x900, 0x00000000);
/* Clear registers */
p_dsp_reg_bank_0[0] = 0;
p_dsp_reg_bank_0[10] = 0;
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(30);
stored_val = read_dsp_ram32(0x900);
if (stored_val == 31)
PASS("STORE wrote 31 to $F1B900");
else
FAIL("STORE: expected 31 at $F1B900, got %u", stored_val);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* Test 13: DSP Stops Itself
* DSP code writes 0 to its own control register to stop execution
* ================================================================ */
static void test_dsp_self_stop(void)
{
uint32_t off;
printf("\n=== Test 13: DSP Self-Stop ===\n");
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
/* Program at $F1B100:
* movei #$F1A114, R10 ; DSP control register address
* moveq #0, R0 ; value 0 (clears DSPGO)
* store R0, (R10) ; write 0 to control -> stops DSP
* NOP sled
*/
write_movei(0x100, DSP_CTRL_ADDR, 10);
write_dsp_ram16(0x106, OP_MOVEQ(0, 0));
/* STORE Rn,(Rm): reg1=Rm=addr, reg2=Rn=value */
write_dsp_ram16(0x108, OP_STORE(10, 0));
p_dsp_reg_bank_0[0] = 0;
p_dsp_reg_bank_0[10] = 0;
p_DSPWriteLong(DSP_PC_ADDR, 0xF1B100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
/* Run plenty of cycles -- DSP should stop itself */
p_DSPExec(100);
if (!p_DSPIsRunning())
PASS("DSP stopped itself by clearing DSPGO");
else
FAIL("DSP still running after self-stop attempt: ctrl=%08X", *p_dsp_control);
}
/* ================================================================
* Test 14: VERSION bits are read-only
* ================================================================ */
static void test_version_readonly(void)
{
uint32_t ctrl_before, ctrl_after;
printf("\n=== Test 14: VERSION Bits Read-Only ===\n");
p_DSPReset();
ctrl_before = *p_dsp_control & VERSION_MASK;
/* Try to overwrite VERSION bits */
p_DSPWriteLong(DSP_CTRL_ADDR, 0x0F000, 6);
ctrl_after = *p_dsp_control & VERSION_MASK;
if (ctrl_before == ctrl_after)
PASS("VERSION bits unchanged after write");
else
FAIL("VERSION bits changed: before=%04X after=%04X", ctrl_before, ctrl_after);
}
/* ================================================================
* Test 15: DSPSetIRQLine INT_LAT5
* Known bug: INT_LAT5 is at bit 16, but the mask calculation in
* DSPSetIRQLine uses INT_LAT0 << irqline, which for irqline=5
* gives bit 11 (0x800, which is BUS_HOG), not bit 16.
* ================================================================ */
static void test_int_lat5(void)
{
uint32_t ctrl;
printf("\n=== Test 15: INT_LAT5 Handling ===\n");
p_DSPReset();
/* Set IRQ line 5 */
p_DSPSetIRQLine(5, 1);
ctrl = *p_dsp_control;
/* INT_LAT5 is at bit 16 (0x10000) */
if (ctrl & INT_LAT5)
PASS("INT_LAT5 (bit 16) correctly set");
else if (ctrl & BUS_HOG)
FAIL("IRQ5 set BUS_HOG (bit 11) instead of INT_LAT5 (bit 16): ctrl=%08X", ctrl);
else
FAIL("IRQ5 didn't set any expected bit: ctrl=%08X", ctrl);
}
/* ================================================================
* Test 16: HLE Boot SSP Initialization
* Verify that non-BIOS boot sets a valid initial stack pointer
* ================================================================ */
static void test_hle_boot_ssp(void)
{
uint8_t **p_jaguarMainRAM;
uint32_t ssp;
printf("\n=== Test 16: HLE Boot SSP ===\n");
p_jaguarMainRAM = dlsym(core_handle, "jaguarMainRAM");
if (!p_jaguarMainRAM || !*p_jaguarMainRAM) {
FAIL("Cannot access jaguarMainRAM");
return;
}
/* Read SSP from RAM[0..3] */
ssp = ((uint32_t)(*p_jaguarMainRAM)[0] << 24)
| ((uint32_t)(*p_jaguarMainRAM)[1] << 16)
| ((uint32_t)(*p_jaguarMainRAM)[2] << 8)
| (uint32_t)(*p_jaguarMainRAM)[3];
if (ssp >= 0x1000 && ssp <= 0x200000)
PASS("SSP is a valid RAM address: $%08X", ssp);
else if (ssp == 0)
FAIL("SSP is zero -- stack operations will corrupt TOM registers");
else
FAIL("SSP is suspicious: $%08X", ssp);
}
/* ================================================================
* Test 17: D_FLAGS store retire window -- register bank (#853)
*
* A DSP-issued store to D_FLAGS does not retire until the instruction
* behind it is already past Read Operands (JTRM v8 p.109: "writing a value
* to the flag bits and making use of those flag bits in the following
* instruction will not work properly due to pipe-lining effects"). So
* when the store flips REGPAGE, the ONE instruction behind it still reads
* AND writes the old bank, and the instruction after that uses the new
* one. Music Demo (ScatoLOGIC) depends on it: `store D_FLAGS; movei r31`
* must set up the bank-0 stack pointer its I2S handler uses.
* ================================================================ */
/* Common setup: NOP-filled RAM, idle loops nowhere near the program, PC at
* $F1B100, bank 0 live, bank-0 r1 = D_FLAGS, bank-0 r0 = REGPAGE. */
static void flags_bank_setup(void)
{
uint32_t off;
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
p_DSPWriteLong(DSP_FLAGS_ADDR, 0, 6); /* host write: bank 0 now */
p_dsp_reg_bank_0[0] = REGPAGE;
p_dsp_reg_bank_0[1] = DSP_FLAGS_ADDR;
}
static void write_idle(uint16_t off)
{
write_dsp_ram16(off, OP_JR(0, -1)); /* jr T,self */
write_dsp_ram16(off + 2, OP_NOP);
}
static void test_flags_store_bank_movei(void)
{
printf("\n=== Test 17: D_FLAGS Store Retire Window (movei behind the store) ===\n");
flags_bank_setup();
/* $F1B100 store r0,(r1) ; D_FLAGS = REGPAGE (bank 0 -> 1)
* $F1B102 movei #$11111111,r5 ; in flight: OLD bank (0)
* $F1B108 movei #$22222222,r6 ; past the window: NEW bank (1)
* $F1B10E jr T,self / nop */
write_dsp_ram16(0x100, OP_STORE(1, 0));
write_movei(0x102, 0x11111111, 5);
write_movei(0x108, 0x22222222, 6);
write_idle(0x10E);
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(12);
if (p_dsp_reg_bank_0[5] == 0x11111111 && p_dsp_reg_bank_1[5] == 0)
PASS("movei behind the D_FLAGS store wrote the pre-store bank (bank-0 r5)");
else
FAIL("movei behind the store: bank0 r5=$%08X bank1 r5=$%08X (expected bank 0)",
p_dsp_reg_bank_0[5], p_dsp_reg_bank_1[5]);
if (p_dsp_reg_bank_1[6] == 0x22222222 && p_dsp_reg_bank_0[6] == 0)
PASS("next instruction, past the window, wrote the new bank (bank-1 r6)");
else
FAIL("instruction after the window: bank0 r6=$%08X bank1 r6=$%08X (expected bank 1)",
p_dsp_reg_bank_0[6], p_dsp_reg_bank_1[6]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
static void test_flags_store_bank_move(void)
{
printf("\n=== Test 18: D_FLAGS Store Retire Window (source operand) ===\n");
flags_bank_setup();
p_dsp_reg_bank_0[2] = 0xAAAA0000;
p_dsp_reg_bank_1[2] = 0xBBBB0000;
/* store r0,(r1); move r2,r7 (old bank in and out); move r2,r8 (new) */
write_dsp_ram16(0x100, OP_STORE(1, 0));
write_dsp_ram16(0x102, OP_MOVE(2, 7));
write_dsp_ram16(0x104, OP_MOVE(2, 8));
write_idle(0x106);
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(12);
if (p_dsp_reg_bank_0[7] == 0xAAAA0000 && p_dsp_reg_bank_1[7] == 0)
PASS("move behind the store read and wrote the pre-store bank");
else
FAIL("move behind the store: bank0 r7=$%08X bank1 r7=$%08X (expected bank0 = $AAAA0000)",
p_dsp_reg_bank_0[7], p_dsp_reg_bank_1[7]);
if (p_dsp_reg_bank_1[8] == 0xBBBB0000 && p_dsp_reg_bank_0[8] == 0)
PASS("move after the window read and wrote the new bank");
else
FAIL("move after the window: bank0 r8=$%08X bank1 r8=$%08X (expected bank1 = $BBBB0000)",
p_dsp_reg_bank_0[8], p_dsp_reg_bank_1[8]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
static void test_flags_store_then_jump(void)
{
printf("\n=== Test 19: D_FLAGS Store Then jump (Wolfenstein 3D epilogue shape) ===\n");
flags_bank_setup();
p_dsp_reg_bank_0[3] = DSP_RAM_BASE + 0x200; /* the real return address */
p_dsp_reg_bank_1[3] = DSP_RAM_BASE + 0x300; /* the wrong one */
/* store r0,(r1); jump T,(r3); moveq #9,r4 (delay slot) */
write_dsp_ram16(0x100, OP_STORE(1, 0));
write_dsp_ram16(0x102, OP_JUMP(0, 3));
write_dsp_ram16(0x104, OP_MOVEQ(9, 4));
write_dsp_ram16(0x200, OP_MOVEQ(7, 5));
write_idle(0x202);
write_dsp_ram16(0x300, OP_MOVEQ(13, 5));
write_idle(0x302);
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(12);
if (p_dsp_reg_bank_1[5] == 7)
PASS("jump behind the store took its target from the pre-store bank");
else
FAIL("jump target: bank0 r5=%u bank1 r5=%u (expected bank1 r5 = 7 from $F1B200)",
p_dsp_reg_bank_0[5], p_dsp_reg_bank_1[5]);
if (p_dsp_reg_bank_1[4] == 9 && p_dsp_reg_bank_0[4] == 0)
PASS("jump delay slot (second instruction behind the store) wrote the new bank");
else
FAIL("jump delay slot: bank0 r4=%u bank1 r4=%u (expected bank 1)",
p_dsp_reg_bank_0[4], p_dsp_reg_bank_1[4]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
static void test_jump_with_flags_store_in_slot(void)
{
printf("\n=== Test 20: jump With D_FLAGS Store In Its Delay Slot (Doom epilogue shape) ===\n");
flags_bank_setup();
p_dsp_reg_bank_0[15] = DSP_RAM_BASE + 0x200;
/* jump T,(r15); store r0,(r1) (delay slot); target: movei #$33333333,r6 */
write_dsp_ram16(0x100, OP_JUMP(0, 15));
write_dsp_ram16(0x102, OP_STORE(1, 0));
write_movei(0x200, 0x33333333, 6);
write_idle(0x206);
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
p_DSPExec(12);
if (p_dsp_reg_bank_1[6] == 0x33333333 && p_dsp_reg_bank_0[6] == 0)
PASS("branch target after a delay-slot D_FLAGS store wrote the new bank");
else
FAIL("branch target: bank0 r6=$%08X bank1 r6=$%08X (expected bank 1)",
p_dsp_reg_bank_0[6], p_dsp_reg_bank_1[6]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
static void test_flags_window_savestate(void)
{
static uint8_t state[65536];
printf("\n=== Test 21: D_FLAGS Retire Window Survives A Savestate ===\n");
if (!p_DSPStateSave || !p_DSPStateLoad) {
printf(" SKIP: DSPStateSave/DSPStateLoad not exported\n");
return;
}
flags_bank_setup();
write_dsp_ram16(0x100, OP_STORE(1, 0));
write_movei(0x102, 0x44444444, 5);
write_idle(0x108);
p_DSPWriteLong(DSP_PC_ADDR, DSP_RAM_BASE + 0x100, 6);
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
/* One cycle = the store alone: the slice ends inside the window. */
p_DSPExec(1);
p_DSPStateSave(state);
/* Scribble the live state, then restore the mid-window snapshot. */
p_DSPWriteLong(DSP_FLAGS_ADDR, 0, 6);
p_DSPStateLoad(state);
p_DSPExec(8);
if (p_dsp_reg_bank_0[5] == 0x44444444 && p_dsp_reg_bank_1[5] == 0)
PASS("instruction behind the store still used the pre-store bank after a load");
else
FAIL("after load: bank0 r5=$%08X bank1 r5=$%08X (expected bank 0)",
p_dsp_reg_bank_0[5], p_dsp_reg_bank_1[5]);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
}
/* ================================================================
* DSP pipeline / gateway timing model (issue #313, timing-campaign S1;
* option virtualjaguar_gpu_pipeline_timing, which now covers the DSP).
*
* Every check is BEHAVIOURAL: a short program is followed by a NOP sled
* and run for a fixed budget B. The sled costs 1 cycle per NOP in
* every configuration, so the cycles the program prefix consumed are
*
* P = k + B - N
*
* where k is the number of instructions the prefix retires and N is the
* dsp_exec_opcode_count delta. On a core without the DSP model the
* option leaves the DSP untouched and P is the legacy
* dsp_opcode_cycles[] cost, so each "model on" assertion fails there.
* Expected values are derived in the comments from the rules in
* docs/gpu-timing-spec.md (R-numbers) and the constants in dsp.c.
* ================================================================ */
/* Leading fields of struct VJSettings (src/core/settings.h). Only the
* first five are touched; their order is pinned by the comment there. */
struct vjs_head { bool ntsc, bios, fastblit, blittiming, pipetiming; };
static struct vjs_head *p_vjs;
static uint32_t *p_dsp_exec_opcode_count;
static uint32_t *p_riscClockScalePct;
static uint8_t *p_busArbiter; /* struct BusArbiter, byte view */
static void (*p_DSPPipeTimingReset)(void);
static void (*p_DSPClockScaleReset)(void);
#define BA_ROW_MISS 0 /* offsetof(struct BusArbiter, dram_row_miss) */
#define BA_ENABLED 4 /* offsetof(struct BusArbiter, enabled) */
#define BA_SCALE 5 /* offsetof(struct BusArbiter, contention_scale) */
#define OP_ADDQ(n, rd) DSP_OP(2, (n), (rd))
#define OP_CMP(rs, rd) DSP_OP(30, (rs), (rd))
#define OP_DIV(rs, rd) DSP_OP(21, (rs), (rd))
#define OP_LOADW(rs, rd) DSP_OP(40, (rs), (rd))
#define OP_LOAD14N(n, rd) DSP_OP(43, (n), (rd))
#define OP_STORE14N(n, rs) DSP_OP(49, (n), (rs))
/* Load prog[] at $F1B000 behind a fresh reset, NOP-sled the rest of
* RAM, apply regs, run budget B with the model on/off, return P. */
static int pipe_run(const uint16_t *prog, int nwords, int k,
const int *regs, int nregs,
int model, int budget)
{
uint32_t off, before;
int i;
p_DSPReset();
for (off = 0; off < 0x2000; off += 2)
write_dsp_ram16(off, OP_NOP);
for (i = 0; i < nwords; i++)
write_dsp_ram16((uint16_t)(i * 2), prog[i]);
for (i = 0; i + 1 < nregs; i += 2)
p_dsp_reg_bank_0[regs[i]] = (uint32_t)regs[i + 1];
p_vjs->pipetiming = model ? true : false;
if (p_DSPPipeTimingReset) p_DSPPipeTimingReset();
if (p_DSPClockScaleReset) p_DSPClockScaleReset();
p_DSPWriteLong(DSP_CTRL_ADDR, DSPGO, 6);
before = *p_dsp_exec_opcode_count;
p_DSPExec(budget);
p_DSPWriteLong(DSP_CTRL_ADDR, 0, 6);
p_vjs->pipetiming = false;
return k + budget - (int)(*p_dsp_exec_opcode_count - before);
}
static void pipe_expect(const char *what, int got, int want)
{
if (got == want)
PASS("%s: prefix cost %d", what, got);
else
FAIL("%s: prefix cost %d, expected %d", what, got, want);
}
static void test_dsp_pipe_model(void)
{
const int B = 200;
int rowmiss, ext_long, ext_word, P;
uint16_t prog[8];
printf("\n=== Test: DSP pipeline timing model (#313) ===\n");
if (!p_vjs || !p_dsp_exec_opcode_count || !p_busArbiter) {
FAIL("missing test-ABI symbols (vjs/dsp_exec_opcode_count/busArbiter)");
return;
}
p_busArbiter[BA_ENABLED] = 0;
p_busArbiter[BA_SCALE] = 1;
if (p_riscClockScalePct)
*p_riscClockScalePct = 100;
rowmiss = p_busArbiter[BA_ROW_MISS];
/* bus_arbiter_dsp_access(): DRAM long = (2 + rowmiss) first word +
* 1 split pause + 2 page-hit second word; word = 2 + rowmiss. */
ext_long = (2 + rowmiss) + 1 + 2;
ext_word = (2 + rowmiss);
/* R2 ALU result-use interlock: add r1,r2 ; addq #1,r2 -> +1. */
prog[0] = OP_ADD(1, 2); prog[1] = OP_ADDQ(1, 2);
P = pipe_run(prog, 2, 2, NULL, 0, 1, B);
pipe_expect("R2 RAW interlock (model on)", P, 3);
P = pipe_run(prog, 2, 2, NULL, 0, 0, B);
pipe_expect("R2 RAW interlock (model off = legacy)", P, 2);
/* Control: one-read consumer of a different register -> no wait. */
prog[1] = OP_ADDQ(1, 3);
P = pipe_run(prog, 2, 2, NULL, 0, 1, B);
pipe_expect("R2 control, independent one-read op", P, 2);
/* §14 write-back port conflict: two reads, neither the target -> +1. */
prog[0] = OP_ADD(1, 2); prog[1] = OP_ADD(4, 3);
P = pipe_run(prog, 2, 2, NULL, 0, 1, B);
pipe_expect("write-back port conflict (model on)", P, 3);
/* R3 flags interlock: cmp ; jr NE (not taken: r1==r2 -> Z) -> +1. */
{
int regs[] = { 1, 5, 2, 5 };
prog[0] = OP_CMP(1, 2); prog[1] = OP_JR(1, 4);
P = pipe_run(prog, 2, 2, regs, 4, 1, B);
pipe_expect("R3 flags interlock, cmp;jr cc (model on)", P, 3);
prog[0] = OP_CMP(1, 2); prog[1] = OP_NOP; prog[2] = OP_JR(1, 4);
P = pipe_run(prog, 3, 3, regs, 4, 1, B);
pipe_expect("R3 control, cmp;nop;jr cc", P, 3);
}
/* R5 local load: 3-tick latency, use right behind it waits 2. */
{
int regs[] = { 1, 0xF1B800 };
prog[0] = OP_LOAD(1, 2); prog[1] = OP_ADD(2, 3);
P = pipe_run(prog, 2, 2, regs, 2, 1, B);
pipe_expect("R5 local load-use (model on)", P, 4);
P = pipe_run(prog, 2, 2, regs, 2, 0, B);
pipe_expect("R5 local load-use (model off = legacy load 2)", P, 3);
}
/* R9 external load through JERRY's 16-bit bus: ready at
* issue 2 + grant 2 + D + return 3 after the load's start; the
* consumer one tick later waits D + 6. Long vs word differ by the
* split transfer (pause 1 + page-hit word 2). */
{
int regs[] = { 1, 0x00100000 };
prog[0] = OP_LOAD(1, 2); prog[1] = OP_ADD(2, 3);
P = pipe_run(prog, 2, 2, regs, 2, 1, B);
pipe_expect("R9 external LONG load-use (model on)", P, 2 + ext_long + 6);
prog[0] = OP_LOADW(1, 2);
P = pipe_run(prog, 2, 2, regs, 2, 1, B);
pipe_expect("R9 external WORD load-use (model on)", P, 2 + ext_word + 6);
prog[0] = OP_LOAD(1, 2);
P = pipe_run(prog, 2, 2, regs, 2, 0, B);
pipe_expect("R9 external load (model off = legacy, no bus cost)", P, 3);
/* Same pair at risc_clock_scale 2x: the wall-time wait doubles in
* DSP cycles; a core-time wait (R5, below) does not. */
if (p_riscClockScalePct)
{
*p_riscClockScalePct = 200;
P = pipe_run(prog, 2, 2, regs, 2, 1, B);
*p_riscClockScalePct = 100;
pipe_expect("R9 external load-use at 2x (wall time scales)", P, 2 + 2 * (ext_long + 6));
}
else
FAIL("riscClockScalePct not exported: 2x wall-time check not run");
/* dram_timing with the model off: immediate self-cost, the GPU's
* GPU_EXT_ACCESS equivalent. */
p_busArbiter[BA_ENABLED] = 1;
P = pipe_run(prog, 2, 2, regs, 2, 0, B);
p_busArbiter[BA_ENABLED] = 0;
pipe_expect("dram_timing self-cost, model off", P, 3 + ext_long);
}
{
int regs[] = { 1, 0xF1B800 };
prog[0] = OP_LOAD(1, 2); prog[1] = OP_ADD(2, 3);
if (p_riscClockScalePct)
{
*p_riscClockScalePct = 200;
P = pipe_run(prog, 2, 2, regs, 2, 1, B);
*p_riscClockScalePct = 100;
pipe_expect("R5 local load-use at 2x (core time does not scale)", P, 4);
}
else
FAIL("riscClockScalePct not exported: 2x core-time check not run");
}
/* A store waits out a pending external load (EXECON / R9). The
* store also takes the write-back port conflict (+1 core) first,
* which is absorbed into the same wait. */
{
int regs[] = { 1, 0x00100000, 4, 0x00100100, 5, 7 };
prog[0] = OP_LOAD(1, 2); prog[1] = OP_STORE(4, 5);
P = pipe_run(prog, 2, 2, regs, 6, 1, B);
pipe_expect("store drains pending external load", P, 1 + (ext_long + 7));
}
/* R15 DIV: 16 background ticks; using the quotient next waits 16. */
{
int regs[] = { 1, 3, 2, 300 };
prog[0] = OP_DIV(1, 2); prog[1] = OP_ADD(2, 3);
P = pipe_run(prog, 2, 2, regs, 4, 1, B);
pipe_expect("R15 divide-use (model on)", P, 18);
P = pipe_run(prog, 2, 2, regs, 4, 0, B);
pipe_expect("R15 divide-use (model off = legacy div 9)", P, 10);
}
/* TOM/JERRY bug 2: indexed-store DATA is not score-boarded, so a
* store of the in-flight quotient does not wait (it stores the old
* value -- dsp_div_store_fixups). Indexed store issue extra +1 only.
* A plain store of the same register IS score-boarded and waits. */
{
int regs[] = { 1, 3, 2, 300, 14, 0xF1B800, 5, 0xF1B810 };
prog[0] = OP_DIV(1, 2); prog[1] = OP_STORE14N(1, 2);
write_dsp_ram32(0x804, 0);
P = pipe_run(prog, 2, 2, regs, 8, 1, B);
pipe_expect("bug 2: div ; store r2,(r14+1) does not stall", P, 3);
if (read_dsp_ram32(0x804) == 300)
PASS("bug 2: indexed store wrote the pre-divide value (300)");
else
FAIL("bug 2: indexed store wrote %08X, expected 300 (old r2)",
read_dsp_ram32(0x804));
prog[1] = OP_STORE(5, 2);
P = pipe_run(prog, 2, 2, regs, 8, 1, B);
pipe_expect("plain store of the quotient waits for the divide", P, 18);
}
/* ...but field 1 of an indexed store IS score-board compared as a
* register number, the immediate offset included (SBOARD.NET:130,
* Srca_reg_ena := OR2 (srcrrd, precomp); issue #895). `store
* r2,(r14+2)` names r2 there, so it waits out the divide and stores
* the quotient 300/3 = 100 -- with the model on or off. */
{
int regs[] = { 1, 3, 2, 300, 14, 0xF1B800 };
int model;
prog[0] = OP_DIV(1, 2); prog[1] = OP_STORE14N(2, 2);
for (model = 1; model >= 0; model--)
{
write_dsp_ram32(0x808, 0);
P = pipe_run(prog, 2, 2, regs, 6, model, B);
if (model)
pipe_expect("bug 2: div ; store r2,(r14+2) waits (offset field = r2)", P, 19);
if (read_dsp_ram32(0x808) == 100)
PASS("bug 2: store r2,(r14+2) wrote the quotient (100), model %s",
model ? "on" : "off");
else
FAIL("bug 2: store r2,(r14+2) wrote %08X, expected 100 (quotient), model %s",
read_dsp_ram32(0x808), model ? "on" : "off");
}
}
/* R16 MOVEI 3 ticks; R6 indexed load +2. */
prog[0] = DSP_OP(38, 0, 5); prog[1] = 0x5678; prog[2] = 0x1234;
P = pipe_run(prog, 3, 1, NULL, 0, 1, B);
pipe_expect("R16 movei (model on)", P, 3);
{
int regs[] = { 14, 0xF1B800 };
prog[0] = OP_LOAD14N(1, 5);
P = pipe_run(prog, 1, 1, regs, 2, 1, B);
pipe_expect("R6 indexed load issue (model on)", P, 3);
}
/* R12/R13 taken jumps: delay slot (1) + refill (JUMP 2, JR 3). */
prog[0] = OP_JR(0, 1);
P = pipe_run(prog, 1, 2, NULL, 0, 1, B);
pipe_expect("R13 taken jr + delay slot (model on)", P, 5);
P = pipe_run(prog, 1, 2, NULL, 0, 0, B);
pipe_expect("taken jr (model off = legacy, delay slot free)", P, 1);
{
int regs[] = { 1, 0xF1B008 };
prog[0] = OP_JUMP(0, 1);
P = pipe_run(prog, 1, 2, regs, 2, 1, B);
pipe_expect("R12 taken jump + delay slot (model on)", P, 4);
}
}
/* ================================================================
* Main
* ================================================================ */
int main(int argc, char *argv[])
{
void *handle;
uint8_t *dummy_rom;
struct retro_game_info game;
(void)argc; (void)argv;
printf("=== Comprehensive DSP Unit Tests ===\n");
handle = dlopen("./" CORE_FILENAME, RTLD_NOW);
if (!handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
core_handle = handle;
#define LOAD(sym) do { \
p_##sym = dlsym(handle, #sym); \
if (!p_##sym) { fprintf(stderr, "Missing: %s\n", #sym); return 1; } \
} while(0)
#define LOAD_OPT(sym) do { p_##sym = dlsym(handle, #sym); } while(0)
LOAD(retro_init);
LOAD(retro_deinit);
LOAD(retro_set_environment);
LOAD(retro_set_video_refresh);
LOAD(retro_set_audio_sample);
LOAD(retro_set_audio_sample_batch);
LOAD(retro_set_input_poll);
LOAD(retro_set_input_state);
LOAD(retro_load_game);
LOAD(retro_unload_game);
LOAD(retro_run);
LOAD(DSPReset);
LOAD(DSPExec);
LOAD(DSPSetIRQLine);
LOAD(DSPIsRunning);
LOAD(DSPInit);
LOAD(DSPGetRAM);
LOAD(DSPWriteLong);
LOAD(DSPReadLong);
LOAD_OPT(DSPStateSave);
LOAD_OPT(DSPStateLoad);
LOAD_OPT(dsp_control);
LOAD_OPT(dsp_pc);
LOAD_OPT(dsp_reg_bank_0);
LOAD_OPT(dsp_reg_bank_1);
LOAD_OPT(dsp_exec_opcode_count);
LOAD_OPT(DSPPipeTimingReset);
LOAD_OPT(DSPClockScaleReset);
p_vjs = (struct vjs_head *)dlsym(handle, "vjs");
p_riscClockScalePct = (uint32_t *)dlsym(handle, "riscClockScalePct");
p_busArbiter = (uint8_t *)dlsym(handle, "busArbiter");
if (!p_dsp_control || !p_dsp_pc || !p_dsp_reg_bank_0) {
fprintf(stderr, "Missing DSP internal symbols\n");
return 1;
}
/* Set up libretro callbacks */
p_retro_set_environment(environment);
p_retro_set_video_refresh(video_refresh);
p_retro_set_audio_sample(audio_sample);
p_retro_set_audio_sample_batch(audio_batch);
p_retro_set_input_poll(input_poll);
p_retro_set_input_state(input_state);
p_retro_init();
/* Create a minimal 128K dummy ROM (JST_ROM detection: size == 131072) */
dummy_rom = calloc(1, 131072);
if (!dummy_rom) {
fprintf(stderr, "Cannot allocate dummy ROM\n");
return 1;
}
/* Set the run address at $800404 to point to $802000 */
dummy_rom[0x404] = 0x00;
dummy_rom[0x405] = 0x80;
dummy_rom[0x406] = 0x20;
dummy_rom[0x407] = 0x00;
/* Put an infinite loop (68K: BRA self = 0x60FE) at $802000 */
dummy_rom[0x2000] = 0x60;
dummy_rom[0x2001] = 0xFE;
memset(&game, 0, sizeof(game));
game.path = "dummy.jag";
game.data = dummy_rom;
game.size = 131072;
if (!p_retro_load_game(&game)) {
fprintf(stderr, "retro_load_game failed with dummy ROM\n");
p_retro_deinit();
free(dummy_rom);
return 1;
}
/* Run tests */
test_dsp_reset_state();
test_dspgo();
test_int_lat_protection();
test_cint_clears_lat();
test_imask_write_protection();
test_register_banking();
test_int_ena_dispatch();
test_cint_before_dispatch();
test_interrupt_priority();
test_interrupt_return_address();
test_dsp_execution();
test_moveq();
test_div_33bit();
test_store_load();
test_dsp_self_stop();
test_version_readonly();
test_int_lat5();
test_hle_boot_ssp();
test_flags_store_bank_movei();
test_flags_store_bank_move();
test_flags_store_then_jump();
test_jump_with_flags_store_in_slot();
test_flags_window_savestate();
test_dsp_pipe_model();
printf("\n=== Results: %d passed, %d failed ===\n", passes, fails);
p_retro_unload_game();
p_retro_deinit();
dlclose(handle);
free(dummy_rom);
return fails > 0 ? 1 : 0;
}