Files
Joseph MattielloandClaude Opus 5 0388c46906 perf(68k): gate the 16-register traceback capture behind a flag (#540)
M68KInstructionHook() runs once per emulated 68K instruction (the call in
m68kinterface.c is guarded only by M68K_HOOK_FUNCTION, which the shipped
release build defines), and its first act was to fill seventeen 1024-entry
traceback rings via seventeen cross-TU m68k_get_reg() calls.

Nothing in a shipped build can read sixteen of them.  vjtrace_backtrace()
and test/tools/cd_wedge_probe.c read pcQueue only; the register rings have
exactly one reader anywhere in the tree (test/test_wmcj_debug.c, and only
a6Queue/d0Queue at that); and a0-a7/d1-d7 are absent from BOTH export
lists, so a production .so/.dylib cannot expose them even to dlsym.

So keep pcQueue unconditional -- it is one store, and it is the ring the
crash-path readers actually use -- and fill the other sixteen only when
startM68KTracing is set.  The flag already existed and was never read;
it is now exported in the test ABI (both exports-test.list for Mach-O and
link-test.T for ELF -- adding one without the other would leave Linux
silently printing $00000000 register columns) and test_wmcj_debug.c sets
it at init, warning loudly rather than falling through to zeros if the
core it loaded does not export it.

Also read regs.pc directly instead of m68k_get_reg(NULL, M68K_REG_PC):
that function is literally `else if (reg == M68K_REG_PC) return regs.pc`,
and jaguar.c already reaches into regs for the bus-charge accounting.

Both halves are semantically no-ops for the emulated machine, so the
frame-hash CSVs must come out byte-identical -- ir_ab.sh checks that on
every arm.

arm64 -O3 codegen for the hook: 146 instructions / 19 calls -> 93 / 2.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 17:58:20 -04:00

347 lines
12 KiB
C

/* test_wmcj_debug.c -- Trace WMCJ boot to inspect CPU/DSP state.
* Build: cc -o test/test_wmcj_debug test/test_wmcj_debug.c -ldl -O0 -g
* Usage: ./test/test_wmcj_debug <core.dylib> <wmcj.jag>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <stdbool.h>
#include <stdarg.h>
#include "../libretro-common/include/libretro.h"
static bool use_bios = false;
static const char *system_dir = "/tmp";
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);
static unsigned (*p_m68k_get_reg)(void *, int);
static uint8_t **p_jaguarMainRAM;
static uint32_t *p_pcQueue;
static uint32_t *p_pcQPtr;
static uint32_t *p_a6Queue;
static uint32_t *p_d0Queue;
/* The 16 register traceback rings are filled only when the core's
* startM68KTracing flag is set -- default off, because filling them costs
* 16 register fetches per emulated 68K instruction (issue #540). This
* tool is their only reader, so it turns them on itself. */
static bool *p_startM68KTracing;
/* DSP registers via TOM/JERRY read functions */
static uint16_t (*p_JERRYReadWord)(uint32_t, uint32_t);
static uint8_t (*p_JERRYReadByte)(uint32_t, uint32_t);
static uint32_t (*p_GPUReadLong)(uint32_t, uint32_t);
static uint8_t *(*p_DSPGetRAM)(void);
static bool (*p_DSPIsRunning)(void);
static uint32_t (*p_DSPReadLong)(uint32_t, uint32_t);
#define M68K_REG_PC 16
#define M68K_REG_A6 14
#define M68K_REG_SP 15
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 lv, const char *fmt, ...) {
va_list ap;
(void)lv;
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 = system_dir;
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 = use_bios ? "enabled" : "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;
}
}
#define LOADSYM(h, name) do { \
*(void **)&p_##name = dlsym(h, #name); \
if (!p_##name) { fprintf(stderr, "dlsym(%s) failed: %s\n", #name, dlerror()); exit(1); } \
} while(0)
#define LOADSYM_OPT(h, name) (*(void **)&p_##name = dlsym(h, #name))
int main(int argc, char **argv)
{
void *handle;
struct retro_game_info info;
FILE *fp;
int frame;
uint32_t pc, sp, a6, qptr;
int i;
{
int a;
for (a = 1; a < argc; a++) {
if (strcmp(argv[a], "--bios") == 0) use_bios = true;
else if (strcmp(argv[a], "--sysdir") == 0 && a+1 < argc) system_dir = argv[++a];
}
}
if (argc < 3) {
fprintf(stderr, "Usage: %s <core.dylib> [--bios] [--sysdir DIR] <rom.jag>\n", argv[0]);
return 1;
}
handle = dlopen(argv[1], RTLD_NOW);
if (!handle) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 1; }
LOADSYM(handle, retro_init);
LOADSYM(handle, retro_deinit);
LOADSYM(handle, retro_set_environment);
LOADSYM(handle, retro_set_video_refresh);
LOADSYM(handle, retro_set_audio_sample);
LOADSYM(handle, retro_set_audio_sample_batch);
LOADSYM(handle, retro_set_input_poll);
LOADSYM(handle, retro_set_input_state);
LOADSYM(handle, retro_load_game);
LOADSYM(handle, retro_unload_game);
LOADSYM(handle, retro_run);
LOADSYM(handle, m68k_get_reg);
LOADSYM_OPT(handle, jaguarMainRAM);
LOADSYM_OPT(handle, pcQueue);
LOADSYM_OPT(handle, pcQPtr);
LOADSYM_OPT(handle, a6Queue);
LOADSYM_OPT(handle, d0Queue);
LOADSYM_OPT(handle, startM68KTracing);
LOADSYM_OPT(handle, JERRYReadWord);
LOADSYM_OPT(handle, JERRYReadByte);
LOADSYM_OPT(handle, GPUReadLong);
LOADSYM_OPT(handle, DSPGetRAM);
LOADSYM_OPT(handle, DSPIsRunning);
LOADSYM_OPT(handle, DSPReadLong);
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();
/* Arm the register traceback rings BEFORE the first retro_run(). Not
* mid-run: pcQPtr advances whether or not tracing is on, so a late flip
* interleaves live registers with zeros against a continuous pcQueue. */
if (p_startM68KTracing)
*p_startM68KTracing = true;
else
fprintf(stderr,
"WARNING: core does not export startM68KTracing -- the A6/D0\n"
" columns below will read $00000000 regardless of what\n"
" the 68K actually held. Rebuild with TEST_EXPORTS=1.\n");
memset(&info, 0, sizeof(info));
info.path = argv[argc - 1];
fp = fopen(argv[argc - 1], "rb");
if (!fp) { perror("fopen"); return 1; }
fseek(fp, 0, SEEK_END);
info.size = ftell(fp);
fseek(fp, 0, SEEK_SET);
info.data = malloc(info.size);
fread((void *)info.data, 1, info.size, fp);
fclose(fp);
if (!p_retro_load_game(&info)) {
fprintf(stderr, "retro_load_game failed\n");
return 1;
}
printf("=== WMCJ boot trace ===\n");
for (frame = 0; frame < 120; frame++) {
p_retro_run();
pc = p_m68k_get_reg(NULL, M68K_REG_PC);
sp = p_m68k_get_reg(NULL, M68K_REG_SP);
a6 = p_m68k_get_reg(NULL, M68K_REG_A6);
if (frame < 15 || frame % 10 == 0 || frame == 119) {
bool dsp_run = p_DSPIsRunning ? p_DSPIsRunning() : false;
uint32_t dsp_val = p_DSPReadLong ? p_DSPReadLong(0xF1B9D8, 0) : 0xDEAD;
printf("Frame %3d: PC=$%08X SP=$%08X DSP=%s $B9D8=$%08X\n",
frame, pc, sp, dsp_run ? "RUN" : "off", dsp_val);
}
}
/* Dump the last 32 entries from pcQueue */
if (p_pcQueue && p_pcQPtr) {
qptr = *p_pcQPtr;
printf("\n=== Last 32 PCs from pcQueue (qptr=%u) ===\n", qptr);
for (i = 31; i >= 0; i--) {
uint32_t idx = (qptr - 1 - i) & 0x3FF;
uint32_t qpc = p_pcQueue[idx];
printf(" [-%2d] PC=$%08X", i, qpc);
if (p_a6Queue)
printf(" A6=$%08X", p_a6Queue[idx]);
if (p_d0Queue)
printf(" D0=$%08X", p_d0Queue[idx]);
printf("\n");
}
}
/* Dump RAM around where the game might be looping */
if (p_jaguarMainRAM) {
uint8_t *ram = *p_jaguarMainRAM;
pc = p_m68k_get_reg(NULL, M68K_REG_PC);
printf("\n=== RAM at PC=$%08X (16 bytes) ===\n", pc);
if (pc < 0x200000) {
for (i = 0; i < 16; i++)
printf("%02X ", ram[pc + i]);
printf("\n");
}
/* Dump vector 64 ($100) */
printf("\n=== Vector 64 ($100) = $%02X%02X%02X%02X ===\n",
ram[0x100], ram[0x101], ram[0x102], ram[0x103]);
/* Dump first 8 bytes of RAM (SP + PC vectors) */
printf("=== SP vector ($0) = $%02X%02X%02X%02X ===\n",
ram[0], ram[1], ram[2], ram[3]);
printf("=== PC vector ($4) = $%02X%02X%02X%02X ===\n",
ram[4], ram[5], ram[6], ram[7]);
}
/* Dump DSP CTRL trace log */
{
struct { uint32_t data, who, old_ctrl, new_ctrl; } *log;
int *log_count;
log = dlsym(handle, "dsp_ctrl_log");
log_count = dlsym(handle, "dsp_ctrl_log_count");
if (log && log_count) {
printf("\n=== DSP_CTRL write log (%d entries) ===\n", *log_count);
for (i = 0; i < *log_count && i < 64; i++)
printf(" [%2d] who=%u data=$%08X old=$%08X → new=$%08X DSPGO=%d→%d\n",
i, log[i].who, log[i].data, log[i].old_ctrl, log[i].new_ctrl,
(log[i].old_ctrl & 1), (log[i].new_ctrl & 1));
}
}
/* Check DSP state */
if (p_JERRYReadWord) {
uint16_t dsp_ctrl_lo = p_JERRYReadWord(0xF1A114, 0);
uint16_t dsp_ctrl_hi = p_JERRYReadWord(0xF1A116, 0);
uint16_t dsp_flags = p_JERRYReadWord(0xF1A100, 0);
printf("\n=== DSP State ===\n");
printf("DSP_CTRL = $%04X%04X (bit0=running: %s)\n",
dsp_ctrl_hi, dsp_ctrl_lo,
(dsp_ctrl_lo & 1) ? "YES" : "NO");
printf("DSP_FLAGS = $%04X\n", dsp_flags);
}
if (p_DSPIsRunning) {
printf("DSPIsRunning() = %s\n", p_DSPIsRunning() ? "YES" : "NO");
}
if (p_DSPReadLong) {
printf("DSP RAM $F1B9D8 via DSPReadLong = $%08X\n",
p_DSPReadLong(0xF1B9D8, 0));
}
if (p_DSPGetRAM) {
uint8_t *dsp_ram = p_DSPGetRAM();
printf("\n=== DSP RAM at $F1B9D0 (32 bytes) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", dsp_ram[0x9D0 + i]);
if ((i & 15) == 15) printf("\n");
}
printf("\n=== DSP RAM at $F1B000 first 64 bytes ===\n");
for (i = 0; i < 64; i++) {
printf("%02X ", dsp_ram[i]);
if ((i & 15) == 15) printf("\n");
}
}
/* Dump RAM around the stuck loop */
if (p_jaguarMainRAM) {
uint8_t *ram = *p_jaguarMainRAM;
printf("\n=== RAM at $15AC0 (32 bytes - stuck loop) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", ram[0x15AC0 + i]);
if ((i & 15) == 15) printf("\n");
}
printf("\n=== RAM at $14220 (32 bytes) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", ram[0x14220 + i]);
if ((i & 15) == 15) printf("\n");
}
/* Also dump some key game workspace areas */
printf("\n=== RAM at $50A0 (32 bytes - game vars) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", ram[0x50A0 + i]);
if ((i & 15) == 15) printf("\n");
}
printf("\n=== Vector 64 ($100) = $%02X%02X%02X%02X ===\n",
ram[0x100], ram[0x101], ram[0x102], ram[0x103]);
}
/* Dump ROM around the loop (info.data still valid) */
{
const uint8_t *rom = (const uint8_t *)info.data;
printf("\n=== ROM at offset $26E0 (32 bytes around loop) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", rom[0x26E0 + i]);
if ((i & 15) == 15) printf("\n");
}
printf("\n=== ROM at offset $2130 (32 bytes around blitter poll) ===\n");
for (i = 0; i < 32; i++) {
printf("%02X ", rom[0x2130 + i]);
if ((i & 15) == 15) printf("\n");
}
}
p_retro_unload_game();
p_retro_deinit();
free((void *)info.data);
dlclose(handle);
return 0;
}