Files
psxrecomp/runtime/src/main.cpp
T
Matthew Stanley 29ebe255bf recomp: safe-state seeds (02918+09890) + turbo mode + Beetle-sourced card2.mcd
Outcomes:
  - YES/NO COPY confirm modal closes correctly on CROSS press (returns to
    COPY menu — matches Beetle behavior)
  - No crash, no freeze
  - Memory card 2 visible with a known-good save (sourced from Beetle's
    dummy.1.mcr; backup of prior card2.mcd kept as .bak.before_beetle_copy)
  - TAB held = turbo mode (skips VRAM->ARGB conversion + SDL_RenderPresent
    every frame except every 30th); useful for navigation cycles

Remaining bug:
  - The actual save data is NOT being copied. Source card content is not
    written to destination. card1.mcd hash unchanged after a copy attempt.
  - 0xBFC0989C still hits psx_unknown_dispatch 60x per copy. Bisect
    confirmed seeding 0xBFC0989C alone causes a misaligned-dispatch crash
    from caller 0xBFC09790 (function-pointer table at runtime addr
    0xA000E1E8 indexed by predicate result, gives garbage target 0x3).
    Seeding both 0xBFC09890 + 0xBFC0989C causes a softlock instead of crash.
    Therefore 0xBFC0989C is intentionally NOT seeded; the copy data path
    short-circuits because the predicate's stale-v0 result fails to enter
    the actual write code.

Status of the function-discovery-miss bug class:
  - VALIDATED for 0xBFC1A1BC (commit 6889f5c) — bandaid worked, modal
    closes
  - PARTIALLY VALIDATED for the BFC09xxx area — adding 0xBFC09890 alone is
    safe but doesn't unblock the copy. 0xBFC0989C cannot be safely seeded
    because it's likely a mid-function label, not a function entry. The
    proper fix requires recompiling the full function containing 0xBFC09790
    (the caller of these predicates) and ensuring the function-pointer
    table at runtime address 0xA000E1E8 is populated with valid recompiled
    function targets.

Turbo mode: hold TAB on the SDL window. Suppresses 29/30 frame presents,
keeps simulated CPU advancing at full speed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:52:55 -07:00

361 lines
13 KiB
C++

/* main.cpp — Phase 3 runtime entry point.
*
* Loads BIOS ROM, initializes CPU state + SDL display, calls into
* the recompiled reset vector. BIOS drives execution; SDL presents
* VRAM at each vblank via callback from gpu_vblank_tick().
*/
#include "cpu_state.h"
#include "psx_interpreter.h"
#include "cdrom.h"
#include "gpu.h"
#include "sio.h"
#include "spu.h"
#include "memcard.h"
#include "debug_server.h"
#include <SDL.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
extern "C" uint64_t gte_get_exec_count(void);
/* memory.c */
extern "C" void memory_init(const char* bios_path);
extern "C" void memory_set_sr_ptr(const uint32_t *p);
/* dma.c */
extern "C" void dma_init(void);
/* timers.c */
extern "C" void timers_init(void);
/* interrupts.c */
extern "C" void interrupts_init(void);
extern "C" uint32_t psx_read_word(uint32_t addr);
extern "C" void psx_write_word(uint32_t addr, uint32_t val);
extern "C" uint16_t psx_read_half(uint32_t addr);
extern "C" void psx_write_half(uint32_t addr, uint16_t val);
extern "C" uint8_t psx_read_byte(uint32_t addr);
extern "C" void psx_write_byte(uint32_t addr, uint8_t val);
/* ---- SDL state ---- */
static SDL_Window* sdl_window;
static SDL_Renderer* sdl_renderer;
static SDL_Texture* sdl_texture;
static uint32_t sdl_pixel_buf[640 * 512]; /* ARGB8888 staging buffer */
/* Convert PS1 16-bit color (1-5-5-5) to ARGB8888 */
static inline uint32_t psx16_to_argb(uint16_t c) {
uint32_t r = (c & 0x1F) << 3;
uint32_t g = ((c >> 5) & 0x1F) << 3;
uint32_t b = ((c >> 10) & 0x1F) << 3;
return 0xFF000000u | (r << 16) | (g << 8) | b;
}
/* PS1 digital pad button bits (active-low: 0=pressed, 1=released).
* Bit 0 = SELECT, Bit 3 = START, Bit 4 = UP, Bit 5 = RIGHT,
* Bit 6 = DOWN, Bit 7 = LEFT, Bit 8 = L2, Bit 9 = R2,
* Bit 10 = L1, Bit 11 = R1, Bit 12 = TRIANGLE, Bit 13 = CIRCLE,
* Bit 14 = CROSS, Bit 15 = SQUARE */
#define PAD_SELECT (1 << 0)
#define PAD_START (1 << 3)
#define PAD_UP (1 << 4)
#define PAD_RIGHT (1 << 5)
#define PAD_DOWN (1 << 6)
#define PAD_LEFT (1 << 7)
#define PAD_L2 (1 << 8)
#define PAD_R2 (1 << 9)
#define PAD_L1 (1 << 10)
#define PAD_R1 (1 << 11)
#define PAD_TRIANGLE (1 << 12)
#define PAD_CIRCLE (1 << 13)
#define PAD_CROSS (1 << 14)
#define PAD_SQUARE (1 << 15)
static void update_pad_from_keyboard(void) {
const Uint8* keys = SDL_GetKeyboardState(NULL);
uint16_t buttons = 0xFFFF; /* all released */
if (keys[SDL_SCANCODE_UP]) buttons &= ~PAD_UP;
if (keys[SDL_SCANCODE_DOWN]) buttons &= ~PAD_DOWN;
if (keys[SDL_SCANCODE_LEFT]) buttons &= ~PAD_LEFT;
if (keys[SDL_SCANCODE_RIGHT]) buttons &= ~PAD_RIGHT;
if (keys[SDL_SCANCODE_RETURN]) buttons &= ~PAD_START;
if (keys[SDL_SCANCODE_RSHIFT]) buttons &= ~PAD_SELECT;
if (keys[SDL_SCANCODE_X]) buttons &= ~PAD_CROSS;
if (keys[SDL_SCANCODE_Z]) buttons &= ~PAD_SQUARE;
if (keys[SDL_SCANCODE_S]) buttons &= ~PAD_CIRCLE;
if (keys[SDL_SCANCODE_A]) buttons &= ~PAD_TRIANGLE;
if (keys[SDL_SCANCODE_Q]) buttons &= ~PAD_L1;
if (keys[SDL_SCANCODE_W]) buttons &= ~PAD_R1;
if (keys[SDL_SCANCODE_E]) buttons &= ~PAD_L2;
if (keys[SDL_SCANCODE_R]) buttons &= ~PAD_R2;
sio_set_pad_state(buttons);
}
/* Called from gpu_vblank_tick() at each simulated vblank. */
static void sdl_vblank_present(void) {
/* Debug server: pause gate, poll commands, record frame, check watchpoints. */
debug_server_wait_if_paused();
debug_server_poll();
debug_server_record_frame();
debug_server_check_watchpoints();
/* Check debug server input override. */
int override = debug_server_get_input_override();
/* Pump SDL events to prevent window freeze. */
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) {
debug_server_shutdown();
std::exit(0);
}
}
/* Sample keyboard state and feed into SIO controller.
* Debug server input override takes priority if active. */
uint16_t pad_buttons_this_frame;
if (override >= 0) {
pad_buttons_this_frame = (uint16_t)override;
} else {
update_pad_from_keyboard();
pad_buttons_this_frame = sio_get_pad_buttons();
}
sio_set_pad_state(pad_buttons_this_frame);
/* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and
* SDL_RenderPresent. The recompiled BIOS still advances simulated
* cycles every vblank, so the BIOS proceeds at whatever rate the host
* CPU sustains without graphics-driver vsync overhead. Present once
* every TURBO_PRESENT_EVERY frames so the user sees visual progress. */
{
const Uint8* keys = SDL_GetKeyboardState(NULL);
static int turbo_skip = 0;
const int TURBO_PRESENT_EVERY = 30;
if (keys[SDL_SCANCODE_TAB]) {
turbo_skip = (turbo_skip + 1) % TURBO_PRESENT_EVERY;
if (turbo_skip != 0) return; /* skip render this frame */
} else {
turbo_skip = 0;
}
}
/* ---- Display from our VRAM ---- */
uint32_t w = 0, h = 0;
{
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0) return;
const uint16_t* vram = gpu_get_vram();
w = di.width; h = di.height;
for (uint32_t y = 0; y < h; y++) {
uint32_t vy = (di.display_y + y) & 511;
for (uint32_t x = 0; x < w; x++) {
uint32_t vx = (di.display_x + x) & 1023;
sdl_pixel_buf[y * w + x] = psx16_to_argb(vram[vy * 1024 + vx]);
}
}
}
/* Update texture and present. */
SDL_UpdateTexture(sdl_texture, NULL, sdl_pixel_buf, (int)(w * sizeof(uint32_t)));
SDL_Rect dst = { 0, 0, 640, 480 };
SDL_RenderClear(sdl_renderer);
SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, &dst);
SDL_RenderPresent(sdl_renderer);
}
int main(int argc, char** argv) {
/* Force line-buffered output so messages appear even if killed. */
std::setvbuf(stdout, nullptr, _IOLBF, 0);
std::setvbuf(stderr, nullptr, _IOLBF, 0);
std::fprintf(stderr, "psxrecomp-v4: main() entered\n");
std::fflush(stderr);
const char* bios_path = "bios/SCPH1001.BIN";
/* Parse positional. First non-flag arg = bios_path. */
for (int i = 1; i < argc; i++) {
if (argv[i][0] != '-') {
bios_path = argv[i];
}
}
std::fprintf(stdout, "psxrecomp-v4 runtime: loading BIOS from %s\n", bios_path);
memory_init(bios_path);
gpu_init();
dma_init();
timers_init();
interrupts_init();
sio_init();
sio_connect_pad(0); /* Controller on port 1 */
spu_init();
cdrom_init(NULL); /* No disc for BIOS-only boot */
memcard_init("."); /* Look for card1.mcd / card2.mcd in working directory */
#ifdef DEFAULT_DEBUG_PORT
debug_server_init(DEFAULT_DEBUG_PORT);
#else
debug_server_init(4370);
#endif
/* ---- SDL init ---- */
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
sdl_window = SDL_CreateWindow(
"psxrecomp-v4 — BIOS",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_SHOWN
);
if (!sdl_window) {
std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
return 1;
}
sdl_renderer = SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED);
if (!sdl_renderer) {
/* Fall back to software renderer. */
sdl_renderer = SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_SOFTWARE);
}
if (!sdl_renderer) {
std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
return 1;
}
sdl_texture = SDL_CreateTexture(
sdl_renderer,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
640, 512
);
if (!sdl_texture) {
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
return 1;
}
/* Register vblank presentation callback. */
gpu_set_vblank_callback(sdl_vblank_present);
/* Initialize CPU state. */
CPUState cpu;
std::memset(&cpu, 0, sizeof(cpu));
/* Wire memory function pointers. */
cpu.read_word = psx_read_word;
cpu.write_word = psx_write_word;
cpu.read_half = psx_read_half;
cpu.write_half = psx_write_half;
cpu.read_byte = psx_read_byte;
cpu.write_byte = psx_write_byte;
/* R3000A reset state. */
cpu.pc = 0xBFC00000u;
cpu.cop0[12] = 0x00400000u; /* SR: BEV=1 (boot exception vectors) */
/* Let memory subsystem see SR for cache-isolation checks. */
memory_set_sr_ptr(&cpu.cop0[12]);
/* Wire debug server to CPU state for register queries. */
debug_server_set_cpu(&cpu);
/* Execute. */
std::fprintf(stdout, "psxrecomp-v4 runtime: executing from PC=0x%08X\n", cpu.pc);
#if defined(PSX_ORACLE_BUILD)
std::fprintf(stdout, "psxrecomp-v4 ORACLE: interpreter mode (port %d)\n", DEFAULT_DEBUG_PORT);
interp_init(&cpu);
interp_trace_enable(1);
/* Breakpoints focused on VSync diagnostics.
* Only break on the VSync incrementer — count how many times it's hit. */
interp_break_add(0x8005A5BCu); /* VSync counter incrementer — KSEG0 */
std::fprintf(stderr, "ORACLE: running with BP at 0x8005A5BC (VSync incrementer)...\n");
std::fflush(stderr);
uint64_t total_executed = 0;
const uint64_t max_total = 100000000ULL; /* 100M instructions */
uint32_t vsync_hits = 0;
for (;;) {
uint32_t ran = interp_step(&cpu, 1000000);
total_executed += ran;
if (interp_hit_breakpoint()) {
vsync_hits++;
if (vsync_hits <= 3 || (vsync_hits % 100 == 0)) {
std::fprintf(stderr, "ORACLE: VSync hit #%u at %llu instructions, ra=0x%08X, gte_exec=%llu\n",
vsync_hits, (unsigned long long)total_executed, cpu.gpr[31],
(unsigned long long)gte_get_exec_count());
/* Dump last 20 trace entries for first 3 hits. */
if (vsync_hits <= 3) {
uint64_t tseq = interp_trace_count();
uint32_t tavail = (tseq < 1048576ULL) ? (uint32_t)tseq : 1048576u;
uint32_t tstart = (tavail > 20) ? tavail - 20 : 0;
for (uint32_t ti = tstart; ti < tavail; ti++) {
const InterpTraceEntry* e = interp_trace_get(ti);
if (e) std::fprintf(stderr, " [%llu] PC=0x%08X insn=0x%08X ra=0x%08X v0=0x%08X\n",
(unsigned long long)e->seq, e->pc, e->insn,
e->gpr[31], e->gpr[2]);
}
}
std::fflush(stderr);
}
/* Step past the breakpoint: temporarily remove, step 1, re-add. */
uint32_t bp_pc = cpu.pc;
interp_break_remove(bp_pc);
interp_step(&cpu, 1);
total_executed++;
interp_break_add(bp_pc);
}
if (ran == 0) {
std::fprintf(stderr, "ORACLE: halted at PC=0x%08X after %llu total instructions\n",
cpu.pc, (unsigned long long)total_executed);
break;
}
if (total_executed >= max_total) {
std::fprintf(stderr, "ORACLE: reached %llu instructions, stopping. PC=0x%08X\n",
(unsigned long long)total_executed, cpu.pc);
/* Dump last 30 trace entries (ring-relative). */
uint64_t tseq2 = interp_trace_count();
uint32_t tavail2 = (tseq2 < 1048576ULL) ? (uint32_t)tseq2 : 1048576u;
uint32_t tstart2 = (tavail2 > 30) ? tavail2 - 30 : 0;
for (uint32_t ti = tstart2; ti < tavail2; ti++) {
const InterpTraceEntry* e = interp_trace_get(ti);
if (e) std::fprintf(stderr, " [%llu] PC=0x%08X insn=0x%08X ra=0x%08X\n",
(unsigned long long)e->seq, e->pc, e->insn, e->gpr[31]);
}
std::fflush(stderr);
break;
}
/* Poll debug server. */
debug_server_poll();
}
/* Read VSync counter from RAM. */
uint32_t vsync_counter = cpu.read_word(0x80079D9Cu);
uint32_t init_flag_48 = cpu.read_word(0x80079D48u);
uint32_t init_flag_4C = cpu.read_word(0x80079D4Cu);
std::fprintf(stderr, "ORACLE: VSync counter = 0x%08X (%u), init_flag@48 = 0x%08X, init_flag@4C = 0x%08X\n",
vsync_counter, vsync_counter, init_flag_48, init_flag_4C);
std::fprintf(stderr, "ORACLE: VSync incrementer hit count = %u\n", vsync_hits);
std::fflush(stderr);
#else
psx_dispatch(&cpu, cpu.pc);
#endif
/* If we reach here, all execution completed without MMIO abort. */
std::fprintf(stdout, "psxrecomp-v4 runtime: execution completed, PC=0x%08X\n", cpu.pc);
debug_server_shutdown();
SDL_DestroyTexture(sdl_texture);
SDL_DestroyRenderer(sdl_renderer);
SDL_DestroyWindow(sdl_window);
SDL_Quit();
return 0;
}