mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
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]>
1535 lines
52 KiB
C
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;
|
|
}
|