test(m68k): lock interrupt-entry supervisor-stack behaviour

No commercial Jaguar title in the local test set ever leaves supervisor
mode (measured: 0 user-mode entries across 45 cart runs and 22 CD runs),
so nothing in the suite exercised the user-mode interrupt path that the
previous commit repaired.  This test drives it directly.

The program seeds USP, drops to user mode via ANDI #$D8FF,SR, pushes a
sentinel longword on the user stack, then the harness raises a level-2
IRQ from C.  The handler records A7 and SR before its RTE.  Assertions:
the frame lands on the supervisor stack, the handler runs privileged, the
user stack contents survive, and the RTE restores the user SP exactly.

Verified discriminating -- run against the pre-fix core it fails on the
two load-bearing assertions with precisely the predicted values:

  handler A7 = $007FF6   (user stack, 6 bytes under the sentinel)
                          expected $008001..$009000
  post-RTE A7 = $008000  (stale regs.usp) expected $007FFC

and against the fixed core all six pass with handler A7 = $008FFA, i.e.
SUPER_STACK_TOP - 6.  make TEST_EXPORTS=1 test: 510 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Fable 5 committed 2026-07-30 00:28:55 -04:00
1 parent cf240af4be
commit 6a2b9398a8
2 files changed
+328 -2

No files matched your search

+7 -2
View File
@@ -744,7 +744,7 @@ $(CORE_DIR)/libretro.o: $(VERSION_H)
clean:
rm -f $(TARGET) $(OBJECTS) $(LINK_MODE_STAMP) \
test/test_cheat test/test_event_queue test/test_blitter_simd \
test/test_dsp_mac40 test/test_m68k_ops test/test_gpu_ops \
test/test_dsp_mac40 test/test_m68k_ops test/test_m68k_irq_ssp test/test_gpu_ops \
test/test_dsp_ops test/test_dsp_unit test/test_hle_bios \
test/test_subsystem_init test/test_subsystem_timeline \
test/test_irq_cascade test/test_boot_patterns test/test_audio_pipeline \
@@ -795,7 +795,7 @@ else
# instead of silently testing the wrong code (see scripts/build-id.sh).
test: export VJ_EXPECT_BUILD := $(shell ./scripts/build-id.sh)
test: test/test_cheat test/test_event_queue test/test_blitter_simd test/test_dsp_mac40 \
$(TARGET) test/test_m68k_ops test/test_gpu_ops test/test_dsp_ops \
$(TARGET) test/test_m68k_ops test/test_m68k_irq_ssp test/test_gpu_ops test/test_dsp_ops \
test/test_dsp_unit test/test_hle_bios test/test_subsystem_init \
test/test_subsystem_timeline test/test_irq_cascade test/test_boot_patterns \
test/test_audio_pipeline test/test_audio_clipping test/test_audio_presence test/test_pit_clock_rate \
@@ -815,6 +815,7 @@ test: test/test_cheat test/test_event_queue test/test_blitter_simd test/test_dsp
./test/test_blitter_simd
./test/test_dsp_mac40
./test/test_m68k_ops
./test/test_m68k_irq_ssp
./test/test_gpu_ops
./test/test_dsp_ops
./test/test_dsp_unit
@@ -944,6 +945,10 @@ test/test_m68k_ops: test/test_m68k_ops.c
$(CC) -O2 -Wall -Wno-unused-function -std=c99 $(INCFLAGS) \
-o $@ test/test_m68k_ops.c -ldl
test/test_m68k_irq_ssp: test/test_m68k_irq_ssp.c
$(CC) -O2 -Wall -Wno-unused-function -std=c99 $(INCFLAGS) \
-o $@ test/test_m68k_irq_ssp.c -ldl
test/test_gpu_ops: test/test_gpu_ops.c
$(CC) -O2 -Wall -Wno-unused-function -std=c99 $(INCFLAGS) \
-o $@ test/test_gpu_ops.c -ldl
+321
View File
@@ -0,0 +1,321 @@
/* test_m68k_irq_ssp.c -- 68000 interrupt entry must use the SUPERVISOR stack.
*
* The 68000 has two A7 registers selected by the S bit. On exception entry it
* copies the SR, sets S -- which makes A7 the supervisor stack pointer -- and
* only then pushes the PC/SR frame (M68000 Programmer's Reference Manual,
* "Exception Processing Sequence"). m68ki_init_exception() used to set
* regs.s = 1 without swapping A7, so an interrupt taken in USER mode stacked
* its frame on the user stack and the handler's RTE restored A7 from a stale
* regs.usp while clobbering regs.isp with the user value.
*
* No commercial Jaguar title in the local test set ever leaves supervisor
* mode, so nothing else in the suite exercises this path -- hence this
* targeted test.
*
* Build: cc -o test/test_m68k_irq_ssp test/test_m68k_irq_ssp.c -ldl
* Usage: ./test/test_m68k_irq_ssp (needs a TEST_EXPORTS=1 core build)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.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
/* 68K register IDs (must match m68kinterface.h enum) */
enum {
M68K_REG_D0 = 0, M68K_REG_D1, M68K_REG_D2, M68K_REG_D3,
M68K_REG_D4, M68K_REG_D5, M68K_REG_D6, M68K_REG_D7,
M68K_REG_A0, M68K_REG_A1, M68K_REG_A2, M68K_REG_A3,
M68K_REG_A4, M68K_REG_A5, M68K_REG_A6, M68K_REG_A7,
M68K_REG_PC, M68K_REG_SR, M68K_REG_SP, M68K_REG_USP
};
/* Memory layout in Jaguar main RAM. The two stacks are a page apart so a
frame pushed on the wrong one is unambiguous from its address alone. */
#define VECTOR_64 0x000100 /* irq_ack_handler() returns 64 for level 2 */
#define RESULT_A7 0x003000 /* A7 as seen inside the handler */
#define RESULT_SR 0x003004 /* SR as seen inside the handler */
#define CODE_BASE 0x004000
#define HANDLER_BASE 0x005000
#define USER_STACK_TOP 0x008000
#define SUPER_STACK_TOP 0x009000
#define SENTINEL 0xDEADBEEF
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 int (*p_m68k_execute)(int);
static void (*p_m68k_set_reg)(int, unsigned int);
static unsigned int (*p_m68k_get_reg)(void *, int);
static void (*p_m68k_set_irq2)(unsigned int);
static uint8_t **p_jaguarMainRAM;
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)
{
struct retro_variable *var;
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:
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
*(const char **)data = "/tmp";
return true;
case RETRO_ENVIRONMENT_GET_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_crash_detect") == 0)
{ var->value = "disabled"; return true; }
var->value = NULL;
return false;
default:
return false;
}
}
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)
static void w16(uint32_t addr, uint16_t v)
{
uint8_t *ram = *p_jaguarMainRAM;
ram[addr] = (uint8_t)((v >> 8) & 0xFF);
ram[addr + 1] = (uint8_t)(v & 0xFF);
}
static void w32(uint32_t addr, uint32_t v)
{
uint8_t *ram = *p_jaguarMainRAM;
ram[addr] = (uint8_t)((v >> 24) & 0xFF);
ram[addr + 1] = (uint8_t)((v >> 16) & 0xFF);
ram[addr + 2] = (uint8_t)((v >> 8) & 0xFF);
ram[addr + 3] = (uint8_t)(v & 0xFF);
}
static uint32_t r32(uint32_t addr)
{
uint8_t *ram = *p_jaguarMainRAM;
return ((uint32_t)ram[addr] << 24) | ((uint32_t)ram[addr + 1] << 16)
| ((uint32_t)ram[addr + 2] << 8) | (uint32_t)ram[addr + 3];
}
/* Build the program. Supervisor prologue drops to user mode, the user body
pushes a sentinel on the user stack, then spins. */
static void build_program(void)
{
uint32_t a = CODE_BASE;
uint32_t h = HANDLER_BASE;
/* MOVE.L #USER_STACK_TOP, A0 */
w16(a, 0x207C); w16(a + 2, USER_STACK_TOP >> 16);
w16(a + 4, USER_STACK_TOP & 0xFFFF); a += 6;
/* MOVE.L A0, USP -- seed the user stack pointer while still privileged */
w16(a, 0x4E60); a += 2;
/* ANDI.W #$D8FF, SR -- clear S (bit 13) and the interrupt mask, so the
level-2 IRQ below is enabled and taken in USER mode. MakeFromSR()
performs the 1->0 swap, leaving A7 = USER_STACK_TOP. */
w16(a, 0x027C); w16(a + 2, 0xD8FF); a += 4;
/* MOVE.L #SENTINEL, D0 */
w16(a, 0x203C); w16(a + 2, SENTINEL >> 16);
w16(a + 4, SENTINEL & 0xFFFF); a += 6;
/* MOVE.L D0, -(A7) -- a real user-stack push the frame must not disturb */
w16(a, 0x2F00); a += 2;
/* BRA.S * -- park here; A7 stays at USER_STACK_TOP-4 */
w16(a, 0x60FE);
/* Handler: record A7 and SR, then return. */
/* MOVE.L A7, D1 (src mode 001 = address register direct, reg 7) */
w16(h, 0x220F); h += 2;
/* MOVE.L D1, RESULT_A7 (absolute long) */
w16(h, 0x23C1); w16(h + 2, RESULT_A7 >> 16);
w16(h + 4, RESULT_A7 & 0xFFFF); h += 6;
/* MOVE.W SR, D2 ; MOVE.L D2, RESULT_SR */
w16(h, 0x40C2); h += 2;
w16(h, 0x23C2); w16(h + 2, RESULT_SR >> 16);
w16(h + 4, RESULT_SR & 0xFFFF); h += 6;
/* RTE */
w16(h, 0x4E73);
w32(VECTOR_64, HANDLER_BASE);
w32(RESULT_A7, 0);
w32(RESULT_SR, 0);
}
int main(int argc, char **argv)
{
void *handle;
uint8_t *dummy_rom;
struct retro_game_info game;
uint32_t user_a7, handler_a7, handler_sr, post_a7, sentinel;
(void)argc; (void)argv;
printf("=== 68K interrupt entry uses the supervisor stack ===\n");
handle = dlopen("./" CORE_FILENAME, RTLD_NOW);
if (!handle) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 1; }
#define LOAD(sym) do { \
p_##sym = dlsym(handle, #sym); \
if (!p_##sym) { fprintf(stderr, "Missing: %s\n", #sym); return 1; } \
} 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(m68k_execute);
LOAD(m68k_set_reg);
LOAD(m68k_get_reg);
LOAD(m68k_set_irq2);
#undef LOAD
p_jaguarMainRAM = (uint8_t **)dlsym(handle, "jaguarMainRAM");
if (!p_jaguarMainRAM || !*p_jaguarMainRAM) {
fprintf(stderr, "Missing jaguarMainRAM\n");
return 1;
}
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();
dummy_rom = (uint8_t *)calloc(1, 131072);
if (!dummy_rom) { fprintf(stderr, "oom\n"); return 1; }
dummy_rom[0x404] = 0x00; dummy_rom[0x405] = 0x80;
dummy_rom[0x406] = 0x20; dummy_rom[0x407] = 0x00;
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\n");
p_retro_deinit(); free(dummy_rom);
return 1;
}
build_program();
/* Enter supervisor mode with a known supervisor stack, then run the
prologue + user body. Order matters: set SR first (so the S bit is
already 1), then A7, so no swap fires behind our back. */
p_m68k_set_reg(M68K_REG_SR, 0x2700);
p_m68k_set_reg(M68K_REG_SP, SUPER_STACK_TOP);
p_m68k_set_reg(M68K_REG_PC, CODE_BASE);
p_m68k_execute(200);
user_a7 = p_m68k_get_reg(NULL, M68K_REG_A7);
if ((p_m68k_get_reg(NULL, M68K_REG_SR) & 0x2000) == 0)
PASS("prologue reached user mode (SR=$%04X)",
p_m68k_get_reg(NULL, M68K_REG_SR));
else
FAIL("prologue still supervisor (SR=$%04X) -- rest of test is void",
p_m68k_get_reg(NULL, M68K_REG_SR));
if (user_a7 == USER_STACK_TOP - 4)
PASS("user stack in use before the IRQ (A7=$%06X)", user_a7);
else
FAIL("expected user A7=$%06X, got $%06X", USER_STACK_TOP - 4, user_a7);
/* Take a level-2 interrupt right here, in user mode. */
p_m68k_set_irq2(2);
p_m68k_execute(200);
handler_a7 = r32(RESULT_A7);
handler_sr = r32(RESULT_SR);
post_a7 = p_m68k_get_reg(NULL, M68K_REG_A7);
sentinel = r32(USER_STACK_TOP - 4);
if (handler_a7 == 0)
FAIL("handler never ran -- no A7 recorded");
else if (handler_a7 > USER_STACK_TOP && handler_a7 <= SUPER_STACK_TOP)
PASS("frame stacked on the supervisor stack (handler A7=$%06X)",
handler_a7);
else
FAIL("frame stacked on the WRONG stack: handler A7=$%06X, expected "
"$%06X..$%06X", handler_a7, USER_STACK_TOP + 1, SUPER_STACK_TOP);
if ((handler_sr & 0x2000) != 0)
PASS("handler runs in supervisor mode (SR=$%04X)", handler_sr & 0xFFFF);
else
FAIL("handler SR=$%04X has S clear", handler_sr & 0xFFFF);
if (sentinel == SENTINEL)
PASS("user stack contents intact across the interrupt");
else
FAIL("user stack clobbered: $%08X at $%06X, expected $%08X",
sentinel, USER_STACK_TOP - 4, SENTINEL);
if (post_a7 == USER_STACK_TOP - 4)
PASS("RTE restored the user stack pointer (A7=$%06X)", post_a7);
else
FAIL("RTE left A7=$%06X, expected $%06X (stale regs.usp)",
post_a7, USER_STACK_TOP - 4);
printf("\n=== Results: %d passed, %d failed ===\n", passes, fails);
free(dummy_rom);
return fails ? 1 : 0;
}