Files
psxrecomp/runtime/src/beetle_libretro.cpp
T
Matthew Stanley 54df1ecf7b Merge feat/audio-quality-pin: measured audio-quality campaign (MMX5 complaint)
Always-on audio tap rings + SPU event ring (runtime + psx-beetle oracle),
audio_stats/audio_wav/audio_events debug commands, offline A/B toolchain.

Fixes, all measured against the Beetle oracle:
- clock-domain DRC bridge for SPU output (pull-mode, polyphase resampler)
- guest-cycle sample budget (768 cyc/sample exact; was 60.00Hz assumption)
- frame pacer bounded catch-up (was forgiving all >1-period debt)
- PI fill servo with anti-windup (was P-only with standing deficit)
- voice loop-address register is LIVE state (X5 stuck-voice +11dB fix)
- CDC decode-volume matrix (X5 music fades)

MMX5: underruns ~360 -> 1 per 150s; level -3.8 dBFS clipping -> -15.5
vs oracle -14.1. Cross-title gate: Tomba built+booted, bridge fill
stable at target, user ear-validated both titles.
2026-07-10 08:13:17 -07:00

1285 lines
55 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* beetle_libretro.cpp — psx-beetle libretro driver + ring buffers.
*
* Drives the Beetle PSX (mednafen-psx) libretro core for psx-beetle.
* Exposes:
* - beetle_init / beetle_shutdown / beetle_run_frame (lifecycle)
* - beetle_get_framebuffer / beetle_get_pad (display + input)
* - beetle_read_byte / beetle_read_word (RAM read)
* - beetle_get_ram / beetle_get_vram / beetle_get_scratchpad
* - SIO trace ring buffer (beetle_get_sio_trace/reset/total)
* - wtrace ring buffer (beetle_wtrace_arm/disarm/reset/get/total/...)
* - fntrace ring buffer (beetle_fntrace_arm/disarm/reset/get/total/...)
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdarg>
#include <csignal>
#include "libretro.h"
#include "mednafen/psx/psx.h"
#include "mednafen/psx/cpu.h"
#include "mednafen/psx/gpu.h"
#include "mednafen/psx/spu.h"
#include "mednafen/psx/cdc.h"
#include "mednafen/psx/frontio.h"
#include "mednafen/psx/irq.h"
#include "beetle_history.h"
#include "audio_trace.h" /* always-on oracle PCM tap ring (port 4380 audio_wav) */
#include "parity_trace.h" /* general control-flow parity ring (oracle producer) */
#include "device_trace.h" /* general device-event cycle ring (oracle producer) */
static void irq_event_callback(int which); /* fwd: device-event ring producer */
/* GPU global lives in PS_GPU's translation unit; psx.h doesn't extern it.
* Declared here for the per-frame display-state snapshot. NOTE: GPU is
* a struct VALUE in gpu.cpp (`PS_GPU GPU;`), not a pointer, despite the
* `extern PS_GPU *GPU;` declaration in beetle-psx/mednafen/psx/debug.cpp.
* That debug.cpp declaration is buggy and would crash if its file were
* actually compiled — we get the value-type declaration right here. */
extern PS_GPU GPU;
namespace {
/* ---- State ---- */
unsigned s_frame_width = 320;
unsigned s_frame_height = 240;
uint16_t s_joypad = 0xFFFF; /* all released (active-low) */
bool s_loaded = false;
uint32_t s_frame_count = 0;
/* ---- SIO byte trace ---- */
#define BEETLE_SIO_TRACE_CAP 65536
struct BeetleSioTraceEntry {
uint32_t seq; uint8_t tx, rx; uint16_t ctrl;
};
static BeetleSioTraceEntry s_sio_trace[BEETLE_SIO_TRACE_CAP];
static int s_sio_trace_idx = 0;
static uint32_t s_sio_trace_seq = 0;
static void sio_trace_callback(uint8_t tx, uint8_t rx, uint16_t ctrl) {
BeetleSioTraceEntry *e = &s_sio_trace[s_sio_trace_idx];
e->seq = s_sio_trace_seq++; e->tx = tx; e->rx = rx; e->ctrl = ctrl;
s_sio_trace_idx = (s_sio_trace_idx + 1) % BEETLE_SIO_TRACE_CAP;
}
/* ---- CD command trace (oracle-diff for the Kula World CD-init wedge) ---- */
#define BEETLE_CDCMD_TRACE_CAP 8192
struct BeetleCdcmdEntry {
uint32_t seq; uint32_t pc; uint8_t cmd, nargs, a0, a1, a2;
};
static BeetleCdcmdEntry s_cdcmd_trace[BEETLE_CDCMD_TRACE_CAP];
static int s_cdcmd_idx = 0;
static uint32_t s_cdcmd_seq = 0;
static void cdcmd_trace_callback(uint8_t cmd, uint8_t nargs,
uint8_t a0, uint8_t a1, uint8_t a2,
uint32_t pc) {
/* Symmetric self-stop trap (mirror of the runtime's PSX_CD_TRAP_CMD):
* SIGSTOP on the Nth matching CD command so a debugger can freeze the
* oracle at the exact boot point and diff its state against ours. */
{
static long trap_cmd = -2, trap_nth = 1, hits = 0;
if (trap_cmd == -2) {
const char *e2 = getenv("PSX_CD_TRAP_CMD");
trap_cmd = (e2 && *e2) ? strtol(e2, NULL, 0) : -1;
const char *e3 = getenv("PSX_CD_TRAP_NTH");
if (e3 && *e3) trap_nth = strtol(e3, NULL, 0);
}
if ((long)cmd == trap_cmd && ++hits == trap_nth) raise(SIGSTOP);
}
BeetleCdcmdEntry *e = &s_cdcmd_trace[s_cdcmd_idx];
e->seq = s_cdcmd_seq++; e->pc = pc; e->cmd = cmd; e->nargs = nargs;
e->a0 = a0; e->a1 = a1; e->a2 = a2;
s_cdcmd_idx = (s_cdcmd_idx + 1) % BEETLE_CDCMD_TRACE_CAP;
}
/* ---- wtrace_all ring (ALWAYS-ON; no filter; captures every write) ----
* Per global rule "ring buffers always-on": every write hits this ring
* unconditionally so probes connected late can still query the last ~1s
* of activity without needing to have armed a range pre-event.
*
* Sizing trade-off: 256K * 32B = 8 MB. At ~250k writes/s idle / ~2M
* writes/s under heavy emulation, retention window is ~1s under load
* and ~1s idle. If the user needs longer retention of specific cells
* they should arm the focused wtrace ring (large capacity, narrow
* filter, longer wrap). The two rings are complementary, NOT
* redundant — focused gives rich register window + long retention for
* armed cells; all gives lean coverage of EVERY write for last second. */
#define BEETLE_WTRACE_ALL_CAP 262144
struct BeetleWtraceAllEntry {
uint64_t seq;
uint32_t addr;
uint32_t new_val;
uint32_t pc;
uint32_t ra;
uint32_t frame;
uint8_t w;
uint8_t pad[3];
};
static BeetleWtraceAllEntry s_wtrace_all[BEETLE_WTRACE_ALL_CAP];
static uint32_t s_wtrace_all_idx = 0;
static uint64_t s_wtrace_all_seq = 0;
/* ---- wtrace ring (filtered by armed physical-RAM ranges) ----
* Parity contract with runtime's WriteTraceEntry: same field names and
* widths on the wire, captured by reading PSX_CPU GPRs at callback time.
*
* Gaps relative to runtime (documented, never zero-faked):
* - old_val: callback fires AFTER the write so the pre-write value is
* already gone. Field omitted from beetle output. Wire it later by
* extending psxrecomp_wtrace_cb_t to receive the old value.
* - func_addr: beetle has no per-function dispatch gate; field omitted.
* - cpu_pc vs pc: identical on beetle (single in-order interpreter), so
* cpu_pc is reported equal to pc rather than duplicated. */
#define BEETLE_WTRACE_CAP 65536
#define BEETLE_WTRACE_MAX_RANGES 16
struct BeetleWtraceEntry {
uint64_t seq;
uint32_t addr;
uint32_t value; /* "new" on the wire */
uint32_t pc;
uint32_t ra;
uint32_t sp;
uint32_t v0, v1;
uint32_t a0, a1, a2, a3;
uint32_t t0, t1;
uint32_t frame;
uint8_t slot_idx; /* beetle-only: current memcard slot byte */
uint8_t size; /* "w" on the wire */
uint8_t pad[2];
};
static BeetleWtraceEntry s_wtrace[BEETLE_WTRACE_CAP];
static uint32_t s_wtrace_idx = 0;
static uint64_t s_wtrace_seq = 0;
static struct { uint32_t lo, hi; } s_wtrace_ranges[BEETLE_WTRACE_MAX_RANGES];
static int s_wtrace_range_count = 0;
static void wtrace_callback(uint32_t addr, uint32_t value,
uint32_t pc, uint32_t ra, uint8_t size)
{
/* Parity last-writer provenance (oracle side): note every write so the
* watch-word last-writer table mirrors the runtime's. pc is the exact store
* PC. No-op unless the parity ring is armed. Identical wire effect to the
* runtime's memory.c hook so parity_diff can compare producers. */
parity_trace_note_write(addr, size, pc);
/* ALWAYS-ON catch-all ring: lean fields, no filter, fires before the
* focused-range filter so a connected probe can read recent writes
* regardless of what's armed. */
{
BeetleWtraceAllEntry *a = &s_wtrace_all[s_wtrace_all_idx];
a->seq = s_wtrace_all_seq++;
a->addr = addr;
a->new_val = value;
a->pc = pc;
a->ra = ra;
a->frame = s_frame_count;
a->w = size;
s_wtrace_all_idx = (s_wtrace_all_idx + 1) % BEETLE_WTRACE_ALL_CAP;
}
if (s_wtrace_range_count == 0) return;
uint32_t phys = addr & 0x1FFFFFFFu;
int hit = 0;
for (int i = 0; i < s_wtrace_range_count; i++) {
uint32_t lo = s_wtrace_ranges[i].lo;
uint32_t hi = s_wtrace_ranges[i].hi;
uint32_t phi = phys + size;
if (phi > lo && phys < hi) { hit = 1; break; }
}
if (!hit) return;
uint8_t slot = 0;
if (MainRAM) slot = MainRAM->data8[0x7264];
BeetleWtraceEntry *e = &s_wtrace[s_wtrace_idx];
e->seq = s_wtrace_seq++;
e->addr = addr;
e->value = value;
e->pc = pc;
e->ra = ra;
e->frame = s_frame_count;
e->slot_idx = slot;
e->size = size;
/* Rich register window — read live from PSX_CPU. Parity with runtime's
* WriteTraceEntry. Only paid on filter hits so volume stays bounded. */
if (PSX_CPU) {
char dummy[8] = {0};
e->sp = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 29, dummy, sizeof(dummy));
e->v0 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 2, dummy, sizeof(dummy));
e->v1 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 3, dummy, sizeof(dummy));
e->a0 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 4, dummy, sizeof(dummy));
e->a1 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 5, dummy, sizeof(dummy));
e->a2 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 6, dummy, sizeof(dummy));
e->a3 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 7, dummy, sizeof(dummy));
e->t0 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 8, dummy, sizeof(dummy));
e->t1 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 9, dummy, sizeof(dummy));
} else {
e->sp = e->v0 = e->v1 = 0;
e->a0 = e->a1 = e->a2 = e->a3 = 0;
e->t0 = e->t1 = 0;
}
s_wtrace_idx = (s_wtrace_idx + 1) % BEETLE_WTRACE_CAP;
}
/* ---- rtrace ring (CPU loads, filtered by armed physical-RAM ranges) ----
* The MMIO-READ counterpart to wtrace. CPU loads are far more voluminous
* than stores, so this ring is ALWAYS range-filtered (there is no always-on
* catch-all twin): arm only the device regs of interest (CD + I_STAT) and the
* ring retains a long window of just those reads. This is THE decisive signal
* for the verifier-vs-lowering classification — what value did the IRQ handler
* actually read from a device register at exception time. Field names mirror
* the runtime's mmio rtrace so one tool reads both ports (Rule 16). */
#define BEETLE_RTRACE_CAP 65536
#define BEETLE_RTRACE_MAX_RANGES 16
struct BeetleRtraceEntry {
uint64_t seq;
uint32_t addr;
uint32_t value; /* "val" on the wire — the loaded (bus) value */
uint32_t pc;
uint32_t ra;
uint32_t frame;
uint8_t size; /* "w" on the wire */
uint8_t pad[3];
};
static BeetleRtraceEntry s_rtrace[BEETLE_RTRACE_CAP];
static uint32_t s_rtrace_idx = 0;
static uint64_t s_rtrace_seq = 0;
static struct { uint32_t lo, hi; } s_rtrace_ranges[BEETLE_RTRACE_MAX_RANGES];
static int s_rtrace_range_count = 0;
static void rtrace_callback(uint32_t addr, uint32_t value,
uint32_t pc, uint32_t ra, uint8_t size)
{
/* Read-side parity provenance (mirror of wtrace_callback's note_write): record
* every CPU load that consumes a watch word, with the exact load PC. No-op
* unless the parity ring is armed. Called BEFORE the rtrace range filter so it
* fires on all loads, just like the runtime's psx_cyc_load_* hook. */
parity_trace_note_read(addr, value, pc);
if (s_rtrace_range_count == 0) return;
uint32_t phys = addr & 0x1FFFFFFFu;
for (int i = 0; i < s_rtrace_range_count; i++) {
uint32_t lo = s_rtrace_ranges[i].lo;
uint32_t hi = s_rtrace_ranges[i].hi;
if (phys + size > lo && phys < hi) {
BeetleRtraceEntry *e = &s_rtrace[s_rtrace_idx];
e->seq = s_rtrace_seq++;
e->addr = addr;
e->value = value;
e->pc = pc;
e->ra = ra;
e->frame = s_frame_count;
e->size = size;
s_rtrace_idx = (s_rtrace_idx + 1) % BEETLE_RTRACE_CAP;
return;
}
}
}
/* ---- fntrace ring (filtered by armed target PCs, or unfiltered) ---- */
#define BEETLE_FNTRACE_CAP 65536
#define BEETLE_FNTRACE_MAX_ARMS 64
struct BeetleFnTraceEntry {
uint64_t seq;
uint32_t caller_pc, target_pc, parent_ra, a0, a1, a2, a3, frame, sp;
uint8_t kind, pad[3];
};
static BeetleFnTraceEntry s_fntrace[BEETLE_FNTRACE_CAP];
static uint32_t s_fntrace_idx = 0;
static uint64_t s_fntrace_seq = 0;
static uint32_t s_fntrace_arms[BEETLE_FNTRACE_MAX_ARMS];
static int s_fntrace_arm_count = 0;
static int s_fntrace_unfiltered = 0;
/* Parity RAM reader: addresses are guest-virtual; mask to phys for
* beetle_read_word (extern "C", defined later) which RAM-decodes below 0x800000. */
extern "C" uint32_t beetle_read_word(uint32_t phys);
static uint32_t beetle_parity_rw(void* /*ctx*/, uint32_t addr) {
return beetle_read_word(addr & 0x1FFFFFFFu);
}
static void fntrace_callback(uint32_t caller_pc, uint32_t target_pc,
uint32_t parent_ra, uint32_t a0, uint32_t a1,
uint8_t kind)
{
/* General parity ring (oracle producer): one DISPATCH row per call/jump while
* current_tcb matches the watched thread, mirroring psx-runtime's fntrace
* hook. Independent of the fntrace arm filter; gated by the cheap armed flag. */
if (parity_trace_is_armed() && PSX_CPU) {
char d[8] = {0};
uint32_t ra = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 31, d, sizeof d);
uint32_t sp = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 29, d, sizeof d);
parity_trace_record(PARITY_KIND_DISPATCH, target_pc, ra, sp, target_pc,
beetle_parity_rw, nullptr);
}
if (!s_fntrace_unfiltered) {
if (s_fntrace_arm_count == 0) return;
uint32_t tphys = target_pc & 0x1FFFFFFFu;
int hit = 0;
for (int i = 0; i < s_fntrace_arm_count; i++)
if (s_fntrace_arms[i] == tphys) { hit = 1; break; }
if (!hit) return;
}
BeetleFnTraceEntry *e = &s_fntrace[s_fntrace_idx];
e->seq = s_fntrace_seq++;
e->caller_pc = caller_pc;
e->target_pc = target_pc;
e->parent_ra = parent_ra;
e->a0 = a0;
e->a1 = a1;
if (PSX_CPU) {
char dummy[8] = {0};
e->a2 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 6, dummy, sizeof(dummy));
e->a3 = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 7, dummy, sizeof(dummy));
} else {
e->a2 = 0;
e->a3 = 0;
}
e->frame = s_frame_count;
e->kind = kind;
/* SP at retire — needed for the func_8001A954 SP/RA-lifecycle oracle
* (does the invocation's stack balance / return). GPR[29] read live. */
{
char dummy[8] = {0};
e->sp = PSX_CPU ? PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + 29, dummy, sizeof(dummy)) : 0;
}
s_fntrace_idx = (s_fntrace_idx + 1) % BEETLE_FNTRACE_CAP;
}
/* ---- Disk control + system dir ---- */
static char s_system_dir[4096] = ".";
static struct retro_disk_control_callback s_disk_cb = {0};
static struct retro_disk_control_ext_callback s_disk_cb_ext = {0};
static int s_have_disk_cb = 0;
static int s_have_disk_cb_ext = 0;
/* ---- Framebuffer capture ---- */
#define BEETLE_FB_MAX_PIXELS (640 * 512)
static uint32_t s_fb[BEETLE_FB_MAX_PIXELS];
static unsigned s_fb_width = 0;
static unsigned s_fb_height = 0;
void on_video_refresh(const void *data, unsigned width, unsigned height, size_t pitch) {
s_frame_width = width;
s_frame_height = height;
if (!data || width == 0 || height == 0) return;
if (width > 640) width = 640;
if (height > 512) height = 512;
s_fb_width = width;
s_fb_height = height;
const uint8_t *src = (const uint8_t *)data;
for (unsigned y = 0; y < height; y++) {
const uint32_t *src_row = (const uint32_t *)(src + y * pitch);
uint32_t *dst_row = &s_fb[y * width];
for (unsigned x = 0; x < width; x++) dst_row[x] = src_row[x];
}
}
/* Beetle's fully-mixed 44.1 kHz reference SPU output arrives here every
* retro_run. Record it into the always-on audio trace (tap SPU_OUT) so port
* 4380's audio_wav can dump oracle PCM for offline A/B against the recomp's
* tap on port 4370. The host still plays no audio from psx-beetle. */
void on_audio_sample(int16_t l, int16_t r) {
int16_t frame[2] = { l, r };
audio_trace_pcm(AUDIO_TAP_SPU_OUT, frame, 1);
}
size_t on_audio_sample_batch(const int16_t *data, size_t frames) {
audio_trace_pcm(AUDIO_TAP_SPU_OUT, data, (int)frames);
return frames;
}
void on_input_poll(void) {}
int16_t on_input_state(unsigned port, unsigned device, unsigned index, unsigned id) {
(void)index;
if (port != 0 || device != RETRO_DEVICE_JOYPAD) return 0;
/* Map libretro button ID → PS1 pad bit. */
static const int lr_to_psx[16] = {
14, 15, 0, 3, 4, 6, 7, 5, 13, 12, 10, 11, 8, 9, 1, 2
};
if (id >= 16) return 0;
int psx_bit = lr_to_psx[id];
return ((s_joypad >> psx_bit) & 1) ? 0 : 1;
}
void on_log_printf(enum retro_log_level level, const char *fmt, ...) {
(void)level;
va_list ap; va_start(ap, fmt);
std::vfprintf(stderr, fmt, ap);
va_end(ap);
}
bool on_environment(unsigned cmd, void *data) {
switch (cmd) {
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY: *(const char**)data = s_system_dir; return true;
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: *(const char**)data = "."; return true;
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: return true;
case RETRO_ENVIRONMENT_GET_VARIABLE: {
struct retro_variable *var = (struct retro_variable *)data;
if (var && var->key) {
if (!strcmp(var->key, "beetle_psx_cpu_dynarec")) { var->value = "disabled"; return true; }
if (!strcmp(var->key, "beetle_psx_skip_bios")) { var->value = "disabled"; return true; }
if (!strcmp(var->key, "beetle_psx_override_bios")) { var->value = "disabled"; return true; }
if (!strcmp(var->key, "beetle_psx_bios_region")) { var->value = "any"; return true; }
if (!strcmp(var->key, "beetle_psx_renderer")) { var->value = "software"; return true; }
if (!strcmp(var->key, "beetle_psx_analog_toggle")) { var->value = "disabled"; return true; }
if (!strcmp(var->key, "beetle_psx_cd_fastload")) { var->value = "2x(native)"; return true; }
if (!strcmp(var->key, "beetle_psx_use_mednafen_memcard0_method")){ var->value = "mednafen"; return true; }
if (!strcmp(var->key, "beetle_psx_enable_memcard1")) { var->value = "enabled"; return true; }
if (!strcmp(var->key, "beetle_psx_shared_memory_cards")) { var->value = "disabled"; return true; }
if (!strcmp(var->key, "beetle_psx_cd_access_method")) { var->value = "sync"; return true; }
}
return false;
}
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE: *(bool*)data = false; return true;
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE: {
struct retro_log_callback *cb = (struct retro_log_callback *)data;
cb->log = on_log_printf;
return true;
}
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
case RETRO_ENVIRONMENT_SET_GEOMETRY:
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
case RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
case RETRO_ENVIRONMENT_GET_PERF_INTERFACE:
case RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE:
return false;
case RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE: {
const struct retro_disk_control_callback *cb =
(const struct retro_disk_control_callback *)data;
if (cb) { s_disk_cb = *cb; s_have_disk_cb = 1; }
return true;
}
case RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE: {
const struct retro_disk_control_ext_callback *cb =
(const struct retro_disk_control_ext_callback *)data;
if (cb) { s_disk_cb_ext = *cb; s_have_disk_cb_ext = 1; }
return true;
}
case RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION: {
unsigned *ver = (unsigned*)data;
if (ver) *ver = 1;
return true;
}
default: return false;
}
}
} /* anonymous namespace */
/* ============================================================ */
/* ============== Public extern "C" interface =================== */
/* ============================================================ */
extern "C" int beetle_init_with_disc(const char *bios_path, const char *disc_path);
extern "C" int beetle_init(const char *bios_path) {
return beetle_init_with_disc(bios_path, NULL);
}
extern "C" int beetle_init_with_disc(const char *bios_path, const char *disc_path) {
if (s_loaded) return 0;
/* Allocate the per-frame history ring once. Mirrors runtime's
* frame_history allocation in debug_server_init(). */
beetle_history_init();
/* Set system directory to the directory containing the BIOS. */
strncpy(s_system_dir, bios_path, sizeof(s_system_dir) - 1);
char *last_sep = strrchr(s_system_dir, '/');
if (!last_sep) last_sep = strrchr(s_system_dir, '\\');
if (last_sep) *last_sep = '\0';
else strcpy(s_system_dir, ".");
retro_set_environment(on_environment);
retro_set_video_refresh(on_video_refresh);
retro_set_audio_sample(on_audio_sample);
retro_set_audio_sample_batch(on_audio_sample_batch);
retro_set_input_poll(on_input_poll);
retro_set_input_state(on_input_state);
std::fprintf(stderr, "[psx-beetle] retro_init...\n"); std::fflush(stderr);
retro_init();
/* If disc_path is non-NULL, load that real disc and keep it inserted.
* Otherwise fall back to the dummy-cue + eject behavior so the BIOS
* shows its shell (memcard editor / CD player). */
char load_path[4096];
if (disc_path && *disc_path) {
strncpy(load_path, disc_path, sizeof(load_path) - 1);
load_path[sizeof(load_path) - 1] = '\0';
} else {
snprintf(load_path, sizeof(load_path), "%s/dummy.cue", s_system_dir);
}
std::fprintf(stderr, "[psx-beetle] retro_load_game(%s)...\n", load_path); std::fflush(stderr);
struct retro_game_info info;
memset(&info, 0, sizeof(info));
info.path = load_path;
if (!retro_load_game(&info)) {
std::fprintf(stderr, "[psx-beetle] retro_load_game failed\n");
retro_deinit();
return -1;
}
s_loaded = true;
s_frame_count = 0;
/* Eject the fake disc so BIOS shows the shell selector — only when no
* real disc was provided. With a real disc, keep it inserted so the
* BIOS proceeds straight into game boot. */
if (!disc_path || !*disc_path) {
if (s_have_disk_cb_ext && s_disk_cb_ext.set_eject_state) {
s_disk_cb_ext.set_eject_state(true);
} else if (s_have_disk_cb && s_disk_cb.set_eject_state) {
s_disk_cb.set_eject_state(true);
}
}
/* Memcard sanity-check (warn-only). */
{
const char *card_files[] = { "dummy.0.mcr", "dummy.1.mcr" };
for (int sl = 0; sl < 2; sl++) {
FILE *f = fopen(card_files[sl], "rb");
if (!f) {
std::fprintf(stderr,
"[psx-beetle] WARNING: %s missing — slot %d will be blank\n",
card_files[sl], sl + 1);
continue;
}
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
unsigned char magic[2] = {0, 0};
fread(magic, 1, 2, f); fclose(f);
int valid = (sz == 131072 && magic[0] == 'M' && magic[1] == 'C');
std::fprintf(stderr,
"[psx-beetle] memcard slot %d: %s size=%ld magic=%c%c %s\n",
sl + 1, card_files[sl], sz,
magic[0]?magic[0]:'?', magic[1]?magic[1]:'?',
valid ? "OK" : "INVALID");
}
}
if (PSX_FIO) {
PSX_FIO->SetSIOTraceCallback(sio_trace_callback);
std::fprintf(stderr, "[psx-beetle] SIO trace callback registered\n");
}
g_psxrecomp_wtrace_cb = wtrace_callback;
g_psxrecomp_fntrace_cb = fntrace_callback;
g_psxrecomp_rtrace_cb = rtrace_callback;
g_psxrecomp_irq_cb = irq_event_callback;
std::fprintf(stderr, "[psx-beetle] wtrace + fntrace + rtrace + irq callbacks registered\n");
g_psxrecomp_cdcmd_cb = cdcmd_trace_callback;
std::fprintf(stderr, "[psx-beetle] wtrace + fntrace + cdcmd callbacks registered\n");
std::fflush(stderr);
return 0;
}
extern "C" void beetle_shutdown(void) {
if (!s_loaded) return;
retro_unload_game();
retro_deinit();
s_loaded = false;
}
extern "C" void beetle_run_frame(uint16_t pad1_buttons) {
if (!s_loaded) return;
s_joypad = pad1_buttons;
s_frame_count++;
audio_trace_note_frame(s_frame_count);
retro_run();
beetle_history_record_frame();
}
extern "C" int beetle_get_framebuffer(uint32_t **out_pixels,
unsigned *out_w, unsigned *out_h) {
if (s_fb_width == 0 || s_fb_height == 0) return 0;
*out_pixels = s_fb;
*out_w = s_fb_width;
*out_h = s_fb_height;
return 1;
}
/* Raw VRAM 15bpp peek (parity with psx-runtime gpu_vram_peek) — for the MDEC
* full-path oracle diff. (x,y) in 1024x512 VRAM; A = (y<<10)|x. */
extern "C" uint16_t beetle_vram_peek(uint32_t x, uint32_t y) {
return GPU_PeekRAM(((y & 0x1FF) << 10) | (x & 0x3FF));
}
extern "C" uint16_t beetle_get_pad(void) { return s_joypad; }
extern "C" uint32_t beetle_get_frame_count(void) { return s_frame_count; }
extern "C" int beetle_is_loaded(void) { return s_loaded ? 1 : 0; }
/* Oracle CPU register snapshot (ground truth via PS_CPU::GetRegister).
* Fills out[0..37]: [0..31]=GPR, [32]=PC, [33]=LO, [34]=HI, [35]=SR(cop0.12),
* [36]=CAUSE(cop0.13), [37]=EPC(cop0.14). Returns 0 if the CPU isn't up yet.
* This closes the get_registers gap so the oracle can serve full CPU state
* for order+state+caller first-divergence (PRINCIPLES.md), matching the
* recomp's get_registers. */
extern "C" int beetle_get_registers(uint32_t *out /* >= 38 words */) {
if (!PSX_CPU || !out) return 0;
char dummy[8] = {0};
for (int i = 0; i < 32; i++)
out[i] = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + (unsigned)i, dummy, sizeof(dummy));
out[32] = PSX_CPU->GetRegister(PS_CPU::GSREG_PC, dummy, sizeof(dummy));
out[33] = PSX_CPU->GetRegister(PS_CPU::GSREG_LO, dummy, sizeof(dummy));
out[34] = PSX_CPU->GetRegister(PS_CPU::GSREG_HI, dummy, sizeof(dummy));
out[35] = PSX_CPU->GetRegister(PS_CPU::GSREG_SR, dummy, sizeof(dummy));
out[36] = PSX_CPU->GetRegister(PS_CPU::GSREG_CAUSE, dummy, sizeof(dummy));
out[37] = PSX_CPU->GetRegister(PS_CPU::GSREG_EPC, dummy, sizeof(dummy));
return 1;
}
extern "C" uint8_t beetle_read_byte(uint32_t phys) {
if (phys < 0x00800000u) {
uint32_t offset = phys & 0x1FFFFF;
if (MainRAM) return MainRAM->data8[offset];
}
if (phys >= 0x1F800000u && phys < 0x1F800400u) {
if (ScratchRAM) return ScratchRAM->data8[phys & 0x3FF];
}
if (phys >= 0x1FC00000u && phys < 0x1FC80000u) {
if (BIOSROM) return BIOSROM->data8[phys & 0x7FFFF];
}
return 0;
}
extern "C" uint32_t beetle_read_word(uint32_t phys) {
if (phys < 0x00800000u) {
uint32_t offset = phys & 0x1FFFFF;
if (MainRAM && offset + 3 < 0x200000) {
uint32_t val; memcpy(&val, MainRAM->data8 + offset, 4); return val;
}
}
if (phys >= 0x1F800000u && phys < 0x1F800400u) {
uint32_t offset = phys & 0x3FF;
if (ScratchRAM && offset + 3 < 1024) {
uint32_t val; memcpy(&val, ScratchRAM->data8 + offset, 4); return val;
}
}
if (phys >= 0x1FC00000u && phys < 0x1FC80000u) {
uint32_t offset = phys & 0x7FFFF;
if (BIOSROM && offset + 3 < 0x80000) {
uint32_t val; memcpy(&val, BIOSROM->data8 + offset, 4); return val;
}
}
return 0;
}
/* ---- Parity trace oracle producer (parity_trace.h): frame + cycle accessors. ---- */
extern "C" uint32_t parity_host_frame(void) { return s_frame_count; }
extern "C" unsigned long long beetle_core_get_guest_cycles(void); /* beetle-psx lib */
extern "C" uint64_t parity_host_cycle(void) { return (uint64_t)beetle_core_get_guest_cycles(); }
/* Device-event ring producer: mednafen IRQ_Assert rising edge -> device_trace.
* device_trace_note() stamps the guest cycle via parity_host_cycle() above and
* is a no-op unless the ring is armed. `which` is the I_STAT bit (same numbering
* as the runtime: 2=CD, 3=DMA). Per-source detail matches native's:
* CD -> low nibble of cdc IRQBuffer (INT type: 1=sector-data,2=complete,3=ack)
* DMA -> DMAIntStatus per-channel bitmask (bit n = channel n completed)
* so devtrace can compare native vs Beetle like-for-like (INT1-sector cadence,
* per-channel DMA-completion cadence). */
extern "C" unsigned psxrecomp_cdc_irq_type(void); /* beetle-psx cdc.cpp */
extern "C" unsigned psxrecomp_dma_int_status(void); /* beetle-psx dma.cpp */
static void irq_event_callback(int which) {
uint32_t detail = 0u;
if (which == 2) detail = psxrecomp_cdc_irq_type(); /* IRQ_CD */
else if (which == 3) detail = psxrecomp_dma_int_status(); /* IRQ_DMA */
device_trace_note((uint32_t)which, detail);
}
extern "C" void beetle_get_ram(uint8_t *out_2mb) {
void *ram = retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM);
size_t ram_size = retro_get_memory_size(RETRO_MEMORY_SYSTEM_RAM);
if (ram && ram_size > 0) {
size_t copy = ram_size < (2u * 1024 * 1024) ? ram_size : (2u * 1024 * 1024);
memcpy(out_2mb, ram, copy);
if (copy < 2u * 1024 * 1024) memset(out_2mb + copy, 0, 2u * 1024 * 1024 - copy);
} else {
memset(out_2mb, 0, 2u * 1024 * 1024);
}
}
extern "C" void beetle_get_vram(uint16_t *out_1mb) {
uint16_t *vram = GPU_get_vram();
if (vram) memcpy(out_1mb, vram, 1024 * 512 * 2);
else memset(out_1mb, 0, 1024 * 512 * 2);
}
extern "C" void beetle_get_scratchpad(uint8_t *out_1kb) {
if (ScratchRAM) memcpy(out_1kb, ScratchRAM->data8, 1024);
else memset(out_1kb, 0, 1024);
}
/* ---- SIO trace accessors ---- */
extern "C" uint32_t beetle_get_sio_trace(uint32_t *out_seq, uint8_t *out_tx,
uint8_t *out_rx, uint16_t *out_ctrl,
int max_count)
{
int avail = (int)(s_sio_trace_seq < (uint32_t)BEETLE_SIO_TRACE_CAP
? s_sio_trace_seq : BEETLE_SIO_TRACE_CAP);
int count = max_count < avail ? max_count : avail;
int start = (s_sio_trace_idx - count + BEETLE_SIO_TRACE_CAP) % BEETLE_SIO_TRACE_CAP;
for (int i = 0; i < count; i++) {
int idx = (start + i) % BEETLE_SIO_TRACE_CAP;
const BeetleSioTraceEntry *e = &s_sio_trace[idx];
out_seq[i] = e->seq;
out_tx[i] = e->tx;
out_rx[i] = e->rx;
out_ctrl[i] = e->ctrl;
}
return (uint32_t)count;
}
extern "C" uint32_t beetle_get_sio_trace_total(void) { return s_sio_trace_seq; }
extern "C" void beetle_reset_sio_trace(void) {
s_sio_trace_idx = 0;
s_sio_trace_seq = 0;
memset(s_sio_trace, 0, sizeof(s_sio_trace));
}
/* ---- CD command trace accessors ---- */
extern "C" uint32_t beetle_get_cdcmd_trace(uint32_t *out_seq, uint8_t *out_cmd,
uint8_t *out_nargs, uint8_t *out_a0,
uint8_t *out_a1, uint8_t *out_a2,
uint32_t *out_pc,
int max_count)
{
int avail = (int)(s_cdcmd_seq < (uint32_t)BEETLE_CDCMD_TRACE_CAP
? s_cdcmd_seq : BEETLE_CDCMD_TRACE_CAP);
int count = max_count < avail ? max_count : avail;
int start = (s_cdcmd_idx - count + BEETLE_CDCMD_TRACE_CAP) % BEETLE_CDCMD_TRACE_CAP;
for (int i = 0; i < count; i++) {
const BeetleCdcmdEntry *e = &s_cdcmd_trace[(start + i) % BEETLE_CDCMD_TRACE_CAP];
out_seq[i] = e->seq;
out_cmd[i] = e->cmd;
out_nargs[i] = e->nargs;
out_a0[i] = e->a0;
out_a1[i] = e->a1;
out_a2[i] = e->a2;
out_pc[i] = e->pc;
}
return (uint32_t)count;
}
extern "C" uint32_t beetle_get_cdcmd_trace_total(void) { return s_cdcmd_seq; }
extern "C" void beetle_reset_cdcmd_trace(void) {
s_cdcmd_idx = 0;
s_cdcmd_seq = 0;
memset(s_cdcmd_trace, 0, sizeof(s_cdcmd_trace));
}
/* ---- wtrace accessors ---- */
extern "C" int beetle_wtrace_arm(uint32_t lo, uint32_t hi) {
if (s_wtrace_range_count >= BEETLE_WTRACE_MAX_RANGES) return -1;
if (lo >= hi) return -2;
int slot = s_wtrace_range_count++;
s_wtrace_ranges[slot].lo = lo & 0x1FFFFFFFu;
s_wtrace_ranges[slot].hi = hi & 0x1FFFFFFFu;
return slot;
}
extern "C" void beetle_wtrace_disarm_all(void) { s_wtrace_range_count = 0; }
extern "C" int beetle_wtrace_disarm(int slot) {
if (slot < 0 || slot >= s_wtrace_range_count) return -1;
/* Compact remaining slots down — matches runtime's wtrace_del semantics. */
for (int i = slot; i < s_wtrace_range_count - 1; i++)
s_wtrace_ranges[i] = s_wtrace_ranges[i + 1];
s_wtrace_range_count--;
return 0;
}
extern "C" int beetle_wtrace_range_count(void) { return s_wtrace_range_count; }
extern "C" int beetle_wtrace_max_ranges(void) { return BEETLE_WTRACE_MAX_RANGES; }
extern "C" int beetle_wtrace_capacity(void) { return BEETLE_WTRACE_CAP; }
extern "C" int beetle_wtrace_get_range(int slot, uint32_t *out_lo, uint32_t *out_hi) {
if (slot < 0 || slot >= s_wtrace_range_count) return -1;
*out_lo = s_wtrace_ranges[slot].lo;
*out_hi = s_wtrace_ranges[slot].hi;
return 0;
}
extern "C" void beetle_wtrace_reset(void) {
s_wtrace_idx = 0; s_wtrace_seq = 0;
memset(s_wtrace, 0, sizeof(s_wtrace));
}
extern "C" uint64_t beetle_wtrace_total(void) { return s_wtrace_seq; }
/* ---- rtrace accessors (MMIO-READ trace) — parity with wtrace verbs ---- */
extern "C" int beetle_rtrace_arm(uint32_t lo, uint32_t hi) {
if (s_rtrace_range_count >= BEETLE_RTRACE_MAX_RANGES) return -1;
lo &= 0x1FFFFFFFu; hi &= 0x1FFFFFFFu;
if (lo >= hi) return -2;
int slot = s_rtrace_range_count++;
s_rtrace_ranges[slot].lo = lo;
s_rtrace_ranges[slot].hi = hi;
return slot;
}
extern "C" void beetle_rtrace_disarm_all(void) { s_rtrace_range_count = 0; }
extern "C" int beetle_rtrace_disarm(int slot) {
if (slot < 0 || slot >= s_rtrace_range_count) return -1;
for (int i = slot; i < s_rtrace_range_count - 1; i++)
s_rtrace_ranges[i] = s_rtrace_ranges[i + 1];
s_rtrace_range_count--;
return 0;
}
extern "C" int beetle_rtrace_range_count(void) { return s_rtrace_range_count; }
extern "C" int beetle_rtrace_capacity(void) { return BEETLE_RTRACE_CAP; }
extern "C" int beetle_rtrace_max_ranges(void) { return BEETLE_RTRACE_MAX_RANGES; }
extern "C" int beetle_rtrace_get_range(int slot, uint32_t *out_lo, uint32_t *out_hi) {
if (slot < 0 || slot >= s_rtrace_range_count) return -1;
*out_lo = s_rtrace_ranges[slot].lo;
*out_hi = s_rtrace_ranges[slot].hi;
return 0;
}
extern "C" void beetle_rtrace_reset(void) {
s_rtrace_idx = 0; s_rtrace_seq = 0;
memset(s_rtrace, 0, sizeof(s_rtrace));
}
extern "C" uint64_t beetle_rtrace_total(void) { return s_rtrace_seq; }
/* Fill the newest `count` entries (oldest-first) into the caller arrays.
* Returns the number filled. Mirrors beetle_wtrace_all_get. */
extern "C" uint32_t beetle_rtrace_get(uint64_t *out_seq, uint32_t *out_addr,
uint32_t *out_val, uint32_t *out_pc,
uint32_t *out_ra, uint32_t *out_frame,
uint8_t *out_size, int count) {
if (count < 1) return 0;
int avail = (int)(s_rtrace_seq < (uint64_t)BEETLE_RTRACE_CAP
? s_rtrace_seq : BEETLE_RTRACE_CAP);
if (count > avail) count = avail;
int start = ((int)s_rtrace_idx - count + BEETLE_RTRACE_CAP) % BEETLE_RTRACE_CAP;
for (int i = 0; i < count; i++) {
int idx = (start + i) % BEETLE_RTRACE_CAP;
const BeetleRtraceEntry *e = &s_rtrace[idx];
out_seq[i] = e->seq;
out_addr[i] = e->addr;
out_val[i] = e->value;
out_pc[i] = e->pc;
out_ra[i] = e->ra;
out_frame[i] = e->frame;
out_size[i] = e->size;
}
return (uint32_t)count;
}
/* ---- wtrace_all (always-on, no-filter) accessors ---- */
extern "C" uint64_t beetle_wtrace_all_total(void) { return s_wtrace_all_seq; }
extern "C" int beetle_wtrace_all_capacity(void) { return BEETLE_WTRACE_ALL_CAP; }
extern "C" void beetle_wtrace_all_reset(void) {
s_wtrace_all_idx = 0; s_wtrace_all_seq = 0;
memset(s_wtrace_all, 0, sizeof(s_wtrace_all));
}
extern "C" uint32_t beetle_wtrace_all_get(uint64_t *out_seq, uint32_t *out_addr,
uint32_t *out_new, uint32_t *out_pc,
uint32_t *out_ra, uint32_t *out_frame,
uint8_t *out_w, int max_count)
{
int avail = (int)(s_wtrace_all_seq < (uint64_t)BEETLE_WTRACE_ALL_CAP
? s_wtrace_all_seq : BEETLE_WTRACE_ALL_CAP);
int count = max_count < avail ? max_count : avail;
int start = ((int)s_wtrace_all_idx - count + BEETLE_WTRACE_ALL_CAP) % BEETLE_WTRACE_ALL_CAP;
for (int i = 0; i < count; i++) {
int idx = (start + i) % BEETLE_WTRACE_ALL_CAP;
const BeetleWtraceAllEntry *e = &s_wtrace_all[idx];
out_seq[i] = e->seq;
out_addr[i] = e->addr;
out_new[i] = e->new_val;
out_pc[i] = e->pc;
out_ra[i] = e->ra;
out_frame[i] = e->frame;
out_w[i] = e->w;
}
return (uint32_t)count;
}
/* Rich-window accessor. Out-params mirror the runtime's WriteTraceEntry
* fields, with the documented gaps (old_val, func_addr) absent. */
extern "C" uint32_t beetle_wtrace_get_rich(uint64_t *out_seq, uint32_t *out_addr,
uint32_t *out_value, uint32_t *out_pc,
uint32_t *out_ra,
uint32_t *out_sp,
uint32_t *out_v0, uint32_t *out_v1,
uint32_t *out_a0, uint32_t *out_a1,
uint32_t *out_a2, uint32_t *out_a3,
uint32_t *out_t0, uint32_t *out_t1,
uint32_t *out_frame,
uint8_t *out_slot, uint8_t *out_size,
int max_count)
{
int avail = (int)(s_wtrace_seq < (uint64_t)BEETLE_WTRACE_CAP
? s_wtrace_seq : BEETLE_WTRACE_CAP);
int count = max_count < avail ? max_count : avail;
int start = ((int)s_wtrace_idx - count + BEETLE_WTRACE_CAP) % BEETLE_WTRACE_CAP;
for (int i = 0; i < count; i++) {
int idx = (start + i) % BEETLE_WTRACE_CAP;
const BeetleWtraceEntry *e = &s_wtrace[idx];
out_seq[i] = e->seq;
out_addr[i] = e->addr;
out_value[i] = e->value;
out_pc[i] = e->pc;
out_ra[i] = e->ra;
out_sp[i] = e->sp;
out_v0[i] = e->v0; out_v1[i] = e->v1;
out_a0[i] = e->a0; out_a1[i] = e->a1;
out_a2[i] = e->a2; out_a3[i] = e->a3;
out_t0[i] = e->t0; out_t1[i] = e->t1;
out_frame[i] = e->frame;
out_slot[i] = e->slot_idx;
out_size[i] = e->size;
}
return (uint32_t)count;
}
/* ---- fntrace accessors ---- */
extern "C" int beetle_fntrace_arm(uint32_t target_pc) {
if (s_fntrace_arm_count >= BEETLE_FNTRACE_MAX_ARMS) return -1;
uint32_t phys = target_pc & 0x1FFFFFFFu;
for (int i = 0; i < s_fntrace_arm_count; i++)
if (s_fntrace_arms[i] == phys) return i;
int slot = s_fntrace_arm_count++;
s_fntrace_arms[slot] = phys;
return slot;
}
extern "C" void beetle_fntrace_disarm_all(void) { s_fntrace_arm_count = 0; }
extern "C" int beetle_fntrace_arm_count(void) { return s_fntrace_arm_count; }
extern "C" uint32_t beetle_fntrace_get_arm(int slot) {
if (slot < 0 || slot >= s_fntrace_arm_count) return 0;
return s_fntrace_arms[slot];
}
extern "C" void beetle_fntrace_set_unfiltered(int on) { s_fntrace_unfiltered = on ? 1 : 0; }
extern "C" void beetle_fntrace_reset(void) {
s_fntrace_idx = 0; s_fntrace_seq = 0;
memset(s_fntrace, 0, sizeof(s_fntrace));
}
extern "C" uint64_t beetle_fntrace_total(void) { return s_fntrace_seq; }
/* ---- SPU event ring (always-on, mirrors recomp's spu_events).
*
* Beetle's spu.cpp calls psxrecomp_beetle_spu_event() at every transition
* we care about: KEYON, KEYOFF, END_STOP (loop-end-no-repeat), END_LOOP
* (loop-end-with-repeat). Each entry stamps frame + voice + envelope
* level + sample address, so the boot-chime timeline can be diffed
* directly against recomp's port-4370 spu_events output. */
/* 1M entries × ~32 B = 32 MB. Sized for full boot-chime capture plus
* minutes of in-game timeline; mirrors recomp's SPU_EVENT_CAP exactly. */
#define BEETLE_SPU_EVENT_CAP (1u << 20)
struct BeetleSpuEvent {
uint64_t seq;
uint32_t frame;
uint32_t addr;
uint16_t env;
uint16_t pitch;
uint16_t vol_l_ctrl;
uint16_t vol_r_ctrl;
uint16_t adsr_lo;
uint16_t adsr_hi;
uint8_t kind; /* 1=KEYON 2=KEYOFF 3=END_STOP 4=END_LOOP */
uint8_t voice;
};
static BeetleSpuEvent s_spu_events[BEETLE_SPU_EVENT_CAP];
static uint32_t s_spu_event_idx = 0;
static uint64_t s_spu_event_seq = 0;
extern "C" void psxrecomp_beetle_spu_event(unsigned kind, unsigned voice,
unsigned addr, unsigned env)
{
/* Snapshot the per-voice raw control registers via PS_SPU peek so the
* payload is structurally identical to recomp's SpuEvent. */
BeetleSpuEvent *e = &s_spu_events[s_spu_event_idx & (BEETLE_SPU_EVENT_CAP - 1u)];
e->seq = s_spu_event_seq++;
e->frame = s_frame_count;
e->addr = addr;
e->env = (uint16_t)env;
e->kind = (uint8_t)kind;
e->voice = (uint8_t)voice;
if (PSX_SPU && voice < 24u) {
char dummy[32]; const uint32_t dl = sizeof(dummy);
unsigned base = 0x8000u | ((unsigned)voice << 8);
e->pitch = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_PITCH & 0xFF), dummy, dl);
e->vol_l_ctrl = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_CTRL_L & 0xFF), dummy, dl);
e->vol_r_ctrl = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_CTRL_R & 0xFF), dummy, dl);
unsigned ctrl = PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_ADSR_CTRL & 0xFF), dummy, dl);
e->adsr_lo = (uint16_t)(ctrl & 0xFFFFu);
e->adsr_hi = (uint16_t)((ctrl >> 16) & 0xFFFFu);
} else {
e->pitch = e->vol_l_ctrl = e->vol_r_ctrl = e->adsr_lo = e->adsr_hi = 0;
}
s_spu_event_idx++;
}
extern "C" uint64_t beetle_spu_event_total(void) { return s_spu_event_seq; }
/* CD-audio volume matrix ground truth (PS_CDC::DecodeVolume; 0x80 = unity).
* Returns 0 if the CDC isn't up. */
extern "C" int beetle_cdc_decode_volume(unsigned out[4])
{
extern PS_CDC *PSX_CDC;
if (!PSX_CDC) return 0;
uint8 m[2][2];
PSX_CDC->PSXRecomp_GetDecodeVolume(m);
out[0] = m[0][0]; /* L -> L */
out[1] = m[0][1]; /* L -> R */
out[2] = m[1][0]; /* R -> L */
out[3] = m[1][1]; /* R -> R */
return 1;
}
extern "C" uint32_t beetle_spu_event_get(uint64_t *out_seq, uint32_t *out_frame,
uint32_t *out_addr, uint16_t *out_env,
uint16_t *out_pitch,
uint16_t *out_vol_l, uint16_t *out_vol_r,
uint16_t *out_adsr_lo, uint16_t *out_adsr_hi,
uint8_t *out_kind, uint8_t *out_voice,
uint32_t max_count)
{
uint64_t avail = s_spu_event_seq < (uint64_t)BEETLE_SPU_EVENT_CAP
? s_spu_event_seq : (uint64_t)BEETLE_SPU_EVENT_CAP;
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
uint32_t start = (s_spu_event_idx + BEETLE_SPU_EVENT_CAP - max_count) & (BEETLE_SPU_EVENT_CAP - 1u);
for (uint32_t i = 0; i < max_count; i++) {
const BeetleSpuEvent *e = &s_spu_events[(start + i) & (BEETLE_SPU_EVENT_CAP - 1u)];
out_seq[i] = e->seq;
out_frame[i] = e->frame;
out_addr[i] = e->addr;
out_env[i] = e->env;
out_pitch[i] = e->pitch;
out_vol_l[i] = e->vol_l_ctrl;
out_vol_r[i] = e->vol_r_ctrl;
out_adsr_lo[i] = e->adsr_lo;
out_adsr_hi[i] = e->adsr_hi;
out_kind[i] = e->kind;
out_voice[i] = e->voice;
}
return max_count;
}
/* ---- SPU register peeks via PS_SPU::GetRegister ----
*
* Mirrors the spu_voices wire protocol psx-runtime exposes on port 4370
* so cross-process diff tooling sees the same JSON schema on both ports.
* Beetle's SPU keeps full ADSR, sweep volumes, distinct StartAddr/CurAddr/
* LoopAddr, and a real BlockEnd latch — so this is the oracle ground truth
* for "is voice v actually voicing right now and where in the sample?"
*/
extern "C" int beetle_spu_get_voice_state(int v,
uint16_t *vol_ctrl_l, uint16_t *vol_ctrl_r,
uint16_t *vol_l, uint16_t *vol_r,
uint16_t *pitch,
uint32_t *start_addr, uint32_t *cur_addr, uint32_t *loop_addr,
uint32_t *adsr_ctrl, uint16_t *adsr_level)
{
if (v < 0 || v >= 24 || !PSX_SPU) return 0;
unsigned base = 0x8000u | ((unsigned)v << 8);
char dummy[32]; const uint32_t dl = sizeof(dummy);
*vol_ctrl_l = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_CTRL_L & 0xFF), dummy, dl);
*vol_ctrl_r = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_CTRL_R & 0xFF), dummy, dl);
*vol_l = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_L & 0xFF), dummy, dl);
*vol_r = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_VOL_R & 0xFF), dummy, dl);
*pitch = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_PITCH & 0xFF), dummy, dl);
*start_addr = PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_STARTADDR & 0xFF), dummy, dl);
*cur_addr = PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_READ_ADDR & 0xFF), dummy, dl);
*loop_addr = PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_LOOP_ADDR & 0xFF), dummy, dl);
*adsr_ctrl = PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_ADSR_CTRL & 0xFF), dummy, dl);
*adsr_level = (uint16_t)PSX_SPU->GetRegister(base + (PS_SPU::GSREG_V0_ADSR_LEVEL & 0xFF), dummy, dl);
return 1;
}
extern "C" int beetle_spu_get_global_state(
uint16_t *spu_ctrl,
uint16_t *main_vol_ctrl_l, uint16_t *main_vol_ctrl_r,
uint16_t *main_vol_l, uint16_t *main_vol_r,
uint32_t *fm_mode, uint32_t *noise_mode, uint32_t *reverb_mode,
uint32_t *voice_on, uint32_t *voice_off, uint32_t *block_end)
{
if (!PSX_SPU) return 0;
char dummy[32]; const uint32_t dl = sizeof(dummy);
*spu_ctrl = (uint16_t)PSX_SPU->GetRegister(PS_SPU::GSREG_SPUCONTROL, dummy, dl);
*main_vol_ctrl_l = (uint16_t)PSX_SPU->GetRegister(PS_SPU::GSREG_MAINVOL_CTRL_L, dummy, dl);
*main_vol_ctrl_r = (uint16_t)PSX_SPU->GetRegister(PS_SPU::GSREG_MAINVOL_CTRL_R, dummy, dl);
*main_vol_l = (uint16_t)PSX_SPU->GetRegister(PS_SPU::GSREG_MAINVOL_L, dummy, dl);
*main_vol_r = (uint16_t)PSX_SPU->GetRegister(PS_SPU::GSREG_MAINVOL_R, dummy, dl);
*fm_mode = PSX_SPU->GetRegister(PS_SPU::GSREG_FM_ON, dummy, dl);
*noise_mode = PSX_SPU->GetRegister(PS_SPU::GSREG_NOISE_ON, dummy, dl);
*reverb_mode = PSX_SPU->GetRegister(PS_SPU::GSREG_REVERB_ON, dummy, dl);
*voice_on = PSX_SPU->GetRegister(PS_SPU::GSREG_VOICEON, dummy, dl);
*voice_off = PSX_SPU->GetRegister(PS_SPU::GSREG_VOICEOFF, dummy, dl);
*block_end = PSX_SPU->GetRegister(PS_SPU::GSREG_BLOCKEND, dummy, dl);
return 1;
}
extern "C" uint32_t beetle_fntrace_get(uint64_t *out_seq,
uint32_t *out_caller, uint32_t *out_target,
uint32_t *out_ra, uint32_t *out_a0,
uint32_t *out_a1, uint32_t *out_a2,
uint32_t *out_a3, uint32_t *out_frame,
uint8_t *out_kind, uint32_t *out_sp,
int max_count)
{
int avail = (int)(s_fntrace_seq < (uint64_t)BEETLE_FNTRACE_CAP
? s_fntrace_seq : BEETLE_FNTRACE_CAP);
int count = max_count < avail ? max_count : avail;
int start = ((int)s_fntrace_idx - count + BEETLE_FNTRACE_CAP) % BEETLE_FNTRACE_CAP;
for (int i = 0; i < count; i++) {
int idx = (start + i) % BEETLE_FNTRACE_CAP;
const BeetleFnTraceEntry *e = &s_fntrace[idx];
out_seq[i] = e->seq;
out_caller[i] = e->caller_pc;
out_target[i] = e->target_pc;
out_ra[i] = e->parent_ra;
out_a0[i] = e->a0;
out_a1[i] = e->a1;
out_a2[i] = e->a2;
out_a3[i] = e->a3;
out_frame[i] = e->frame;
out_kind[i] = e->kind;
out_sp[i] = e->sp;
}
return (uint32_t)count;
}
/* ====================================================================== */
/* ==================== PER-FRAME HISTORY RING ======================== */
/* ====================================================================== */
/* Mirrors runtime/src/debug_server.c's s_frame_history. Same capacity,
* same recording cadence (post-frame), same snapshot model
* (4 configurable regions × 128 bytes per frame).
*
* Why duplicate the runtime's mechanism instead of sharing code: psx-beetle
* has no recompiler CPUState; it reads MIPS state via PSX_CPU::GetRegister
* and RAM via the MainRAM byte array. The runtime's recorder reads s_cpu
* (recomp CPUState) and psx_read_byte. Different sources, same wire shape. */
static BeetleFrameRecord *s_beetle_history = nullptr;
static uint64_t s_beetle_history_count = 0;
static uint32_t s_beetle_snap_addrs[BEETLE_RAM_SNAPSHOT_REGIONS];
static int s_beetle_snap_active[BEETLE_RAM_SNAPSHOT_REGIONS];
extern "C" void beetle_history_init(void) {
if (s_beetle_history) return;
s_beetle_history = (BeetleFrameRecord *)std::calloc(
BEETLE_FRAME_HISTORY_CAP, sizeof(BeetleFrameRecord));
s_beetle_history_count = 0;
std::memset(s_beetle_snap_addrs, 0, sizeof(s_beetle_snap_addrs));
std::memset(s_beetle_snap_active, 0, sizeof(s_beetle_snap_active));
if (!s_beetle_history) {
std::fprintf(stderr,
"[psx-beetle] WARNING: failed to allocate %u-frame history ring\n",
(unsigned)BEETLE_FRAME_HISTORY_CAP);
}
}
extern "C" void beetle_history_record_frame(void) {
if (!s_beetle_history || !PSX_CPU) return;
uint32_t idx = (uint32_t)(s_frame_count % BEETLE_FRAME_HISTORY_CAP);
BeetleFrameRecord *r = &s_beetle_history[idx];
r->frame_number = s_frame_count;
r->verify_pass = -1; /* no cross-check verify mode on beetle */
r->diff_count = 0;
/* MIPS CPU state via PS_CPU::GetRegister. The `special` buffer is
* unused by the GPR/HI/LO/COP0-SR/CAUSE/EPC paths in cpu.cpp, but
* we pass a real backing array so future register kinds that DO
* print into it can't smash the stack. */
char dummy[64] = {0};
for (int i = 0; i < 32; i++) {
r->gpr[i] = PSX_CPU->GetRegister(
(unsigned)(PS_CPU::GSREG_GPR + i), dummy, sizeof(dummy));
}
r->hi = PSX_CPU->GetRegister(PS_CPU::GSREG_HI, dummy, sizeof(dummy));
r->lo = PSX_CPU->GetRegister(PS_CPU::GSREG_LO, dummy, sizeof(dummy));
r->cop0_sr = PSX_CPU->GetRegister(PS_CPU::GSREG_SR, dummy, sizeof(dummy));
r->cop0_cause = PSX_CPU->GetRegister(PS_CPU::GSREG_CAUSE, dummy, sizeof(dummy));
r->cop0_epc = PSX_CPU->GetRegister(PS_CPU::GSREG_EPC, dummy, sizeof(dummy));
/* Interrupt controller. */
r->i_stat = IRQ_GetRegister(IRQ_GSREG_STATUS, dummy, sizeof(dummy));
r->i_mask = IRQ_GetRegister(IRQ_GSREG_MASK, dummy, sizeof(dummy));
/* GPU display state. PS_GPU's fields are public. We snapshot the
* raw register-level XStart/YStart for origin; framebuffer width/height
* already mirrored by on_video_refresh into s_fb_*. */
r->display_area_x = (uint16_t)GPU.DisplayFB_XStart;
r->display_area_y = (uint16_t)GPU.DisplayFB_YStart;
r->display_w = (uint16_t)s_fb_width;
r->display_h = (uint16_t)s_fb_height;
r->display_disabled = GPU.DisplayOff ? 1 : 0;
/* SIO / pad. pad_buttons follows runtime convention (active-low
* 16-bit mask). sio_stat/sio_ctrl left zero; beetle's libretro
* surface doesn't expose them and inventing values would obscure
* real divergences in the time-series. */
r->pad_buttons = s_joypad;
r->sio_stat = 0;
r->sio_ctrl = 0;
/* Timing: beetle has no dispatch table to count gates. Leave zero
* dispatch_count; total_dispatches = frame# so the field is
* monotonic and frame_timeseries can still graph it. */
r->dispatch_count = 0;
r->total_dispatches = s_frame_count;
/* Per-frame configurable RAM snapshot regions. */
for (int i = 0; i < BEETLE_RAM_SNAPSHOT_REGIONS; i++) {
r->snapshot_addr[i] = s_beetle_snap_addrs[i];
if (s_beetle_snap_active[i] && s_beetle_snap_addrs[i] != 0 && MainRAM) {
uint32_t base = s_beetle_snap_addrs[i] & 0x1FFFFFu;
for (int j = 0; j < BEETLE_RAM_SNAPSHOT_SIZE; j++) {
uint32_t off = (base + (uint32_t)j) & 0x1FFFFFu;
r->snapshot_data[i][j] = MainRAM->data8[off];
}
} else {
std::memset(r->snapshot_data[i], 0, BEETLE_RAM_SNAPSHOT_SIZE);
}
}
s_beetle_history_count = (uint64_t)s_frame_count + 1;
}
extern "C" void beetle_history_get_bounds(uint64_t *out_count,
uint64_t *out_oldest,
uint64_t *out_newest)
{
uint64_t cnt = s_beetle_history_count;
uint64_t oldest = (cnt > BEETLE_FRAME_HISTORY_CAP)
? cnt - BEETLE_FRAME_HISTORY_CAP : 0;
uint64_t newest = (cnt > 0) ? cnt - 1 : 0;
if (out_count) *out_count = cnt;
if (out_oldest) *out_oldest = oldest;
if (out_newest) *out_newest = newest;
}
extern "C" const BeetleFrameRecord *beetle_history_get_frame(uint32_t frame) {
if (!s_beetle_history) return nullptr;
uint64_t cnt = s_beetle_history_count;
uint64_t oldest = (cnt > BEETLE_FRAME_HISTORY_CAP)
? cnt - BEETLE_FRAME_HISTORY_CAP : 0;
if ((uint64_t)frame < oldest || (uint64_t)frame >= cnt) return nullptr;
uint32_t idx = frame % BEETLE_FRAME_HISTORY_CAP;
const BeetleFrameRecord *r = &s_beetle_history[idx];
if (r->frame_number != frame) return nullptr; /* slot reused since */
return r;
}
extern "C" int beetle_history_first_failure(uint32_t *out_frame, int *out_diff_count) {
/* No verify-mode on beetle yet; mirrors runtime's "no failures" path. */
if (out_frame) *out_frame = (uint32_t)-1;
if (out_diff_count) *out_diff_count = 0;
return 0;
}
extern "C" int beetle_history_set_snapshot(int slot, uint32_t addr) {
if (slot < 0 || slot >= BEETLE_RAM_SNAPSHOT_REGIONS) return 0;
s_beetle_snap_addrs[slot] = addr;
s_beetle_snap_active[slot] = (addr != 0) ? 1 : 0;
return 1;
}
extern "C" int beetle_history_get_snapshot(int slot, uint32_t *out_addr, int *out_active) {
if (slot < 0 || slot >= BEETLE_RAM_SNAPSHOT_REGIONS) return 0;
if (out_addr) *out_addr = s_beetle_snap_addrs[slot];
if (out_active) *out_active = s_beetle_snap_active[slot];
return 1;
}