From 82aeca98a794489fd565bf6992b146bfd3b613c4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 25 Apr 2026 00:15:01 -0700 Subject: [PATCH] feat: embed Beetle PSX libretro core as golden oracle for RAM comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new psx-beetleoracle build target that embeds beetle-psx-libretro (mednafen PSX core) alongside the recompiled BIOS. Both execute the same SCPH1001.BIN and advance in lock-step, enabling byte-level RAM comparison via the TCP debug server. New infrastructure: - psx_oracle_backend.h: abstract oracle backend interface (init, run_frame, get_ram, get_vram, get_scratchpad, read_byte/word, get_cpu_regs, sync_to_state) - beetle_psx_bridge.cpp: libretro bridge with CPU register access via PSX_CPU->GetRegister(), VRAM via GPU_get_vram(), scratchpad via ScratchRAM - psx_oracle_cmds.c: TCP commands — find_first_divergence, emu_read_ram, emu_read_vram, emu_read_scratchpad, emu_cpu_regs, emu_step, emu_sync, emu_sync_press - psx_oracle_stub.c: no-op stubs for non-oracle builds - tools/oracle_diff.py: standalone Python RAM comparison tool - tools/build_oracle.sh: build script for beetle-psx + oracle target Also includes: SIO card probe diagnostic counters (mc_probes, mc_acks, mc_cmds, tx_writes), enhanced dispatch miss logging with register context, memory_get_ram_ptr() for oracle RAM comparison. Build: cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0 cd runtime/build && cmake --build . --target psx-beetleoracle Co-Authored-By: Claude Opus 4.6 (1M context) --- runtime/CMakeLists.txt | 298 +++++++++++++++++-- runtime/include/psx_oracle_backend.h | 65 +++++ runtime/src/beetle_psx_bridge.cpp | 420 +++++++++++++++++++++++++++ runtime/src/debug_server.c | 30 +- runtime/src/main.cpp | 27 +- runtime/src/memory.c | 3 + runtime/src/psx_oracle_cmds.c | 356 +++++++++++++++++++++++ runtime/src/psx_oracle_stub.c | 25 ++ runtime/src/sio.c | 21 ++ runtime/src/traps.c | 3 +- tools/build_oracle.sh | 60 ++++ tools/oracle_diff.py | 154 ++++++++++ 12 files changed, 1441 insertions(+), 21 deletions(-) create mode 100644 runtime/include/psx_oracle_backend.h create mode 100644 runtime/src/beetle_psx_bridge.cpp create mode 100644 runtime/src/psx_oracle_cmds.c create mode 100644 runtime/src/psx_oracle_stub.c create mode 100644 tools/build_oracle.sh create mode 100644 tools/oracle_diff.py diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 9f70f323..42b6b997 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -24,6 +24,8 @@ set(PSX_SHARED_SOURCES src/spu.c src/iso_reader.cpp src/iso_reader_c.cpp + src/psx_oracle_stub.c + src/psx_oracle_cmds.c ) # ---- Native target: recompiled BIOS, stub interpreter ---- @@ -39,29 +41,65 @@ target_include_directories(psx-runtime PRIVATE include) target_compile_definitions(psx-runtime PRIVATE PSX_NATIVE_BUILD=1 DEFAULT_DEBUG_PORT=4370 + $<$:SDL_MAIN_HANDLED> ) -# ---- Oracle target: R3000A interpreter, no generated code ---- +# MSVC-specific: disable /GS (deep recursion triggers false overrun) +# and commit the full 64MB stack (default 4KB commit causes guard page faults +# during deep psx_dispatch recursion). +if(MSVC) + # /GS- : disable stack cookie (deep recursion false positive) + # /guard:cf- : disable CFG (large dispatch table) + # Use static CRT (/MT) instead of DLL CRT (/MD) — the release DLL CRT's + # __report_rangecheckfailure handler triggers __fastfail on our generated + # code's deep recursion pattern. + target_compile_options(psx-runtime PRIVATE /GS- /guard:cf-) + target_link_options(psx-runtime PRIVATE /STACK:67108864,67108864 /GUARD:NO) +endif() -add_executable(psx-oracle - ${PSX_SHARED_SOURCES} - src/psx_interpreter.c - ../generated/SCPH1001_full.c - ../generated/SCPH1001_dispatch.c -) +# ---- Oracle target: R3000A interpreter (MinGW only — not built under MSVC) ---- -target_include_directories(psx-oracle PRIVATE include) -target_compile_definitions(psx-oracle PRIVATE - PSX_ORACLE_BUILD=1 - DEFAULT_DEBUG_PORT=4371 -) +if(NOT MSVC) + add_executable(psx-oracle + ${PSX_SHARED_SOURCES} + src/psx_interpreter.c + ../generated/SCPH1001_full.c + ../generated/SCPH1001_dispatch.c + ) + + target_include_directories(psx-oracle PRIVATE include) + target_compile_definitions(psx-oracle PRIVATE + PSX_ORACLE_BUILD=1 + DEFAULT_DEBUG_PORT=4371 + ) +endif() + +# ---- SDL2 detection ---- + +if(MSVC) + # MSVC: use pre-downloaded SDL2 development package. + file(GLOB SDL2_MSVC_DIR "${CMAKE_SOURCE_DIR}/../sdl2-msvc/SDL2-*") + if(SDL2_MSVC_DIR) + set(SDL2_INCLUDE_DIRS "${SDL2_MSVC_DIR}/include") + set(SDL2_LIBRARIES "${SDL2_MSVC_DIR}/lib/x64/SDL2.lib") + message(STATUS "SDL2 MSVC: ${SDL2_MSVC_DIR}") + else() + message(FATAL_ERROR "SDL2 MSVC dev package not found. Run: bash tools/build_oracle.sh") + endif() +else() + # MinGW: use pkg-config. + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL2 REQUIRED sdl2) +endif() # ---- Common dependencies ---- -foreach(target psx-runtime psx-oracle) - # SDL2 for display presentation. - find_package(PkgConfig REQUIRED) - pkg_check_modules(SDL2 REQUIRED sdl2) +set(PSX_TARGETS psx-runtime) +if(NOT MSVC) + list(APPEND PSX_TARGETS psx-oracle) +endif() + +foreach(target ${PSX_TARGETS}) target_include_directories(${target} PRIVATE ${SDL2_INCLUDE_DIRS}) target_link_libraries(${target} PRIVATE ${SDL2_LIBRARIES}) @@ -71,7 +109,233 @@ foreach(target psx-runtime psx-oracle) endif() # Large stack for deep call chains during BIOS execution. - if(MINGW OR WIN32) + if(MINGW) target_link_options(${target} PRIVATE -Wl,--stack,67108864) + elseif(MSVC) + target_link_options(${target} PRIVATE /STACK:67108864,67108864) endif() endforeach() + +# ---- Beetle PSX oracle target (MinGW, same toolchain) ---- +# +# Embeds beetle-psx-libretro as a golden oracle for byte-level RAM comparison. +# Requires beetle-psx built via: cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0 + +set(BEETLE_LIB "${CMAKE_SOURCE_DIR}/../beetle-psx/libmednafen_psx.a") + +if(NOT MSVC AND EXISTS "${BEETLE_LIB}") + message(STATUS "Beetle PSX oracle: found ${BEETLE_LIB}") + + # libretro-common sources needed by beetle-psx's static lib. + set(BEETLE_DIR "${CMAKE_SOURCE_DIR}/../beetle-psx") + set(LRC "${BEETLE_DIR}/libretro-common") + set(LIBRETRO_COMMON_SOURCES + ${LRC}/streams/file_stream.c + ${LRC}/streams/file_stream_transforms.c + ${LRC}/vfs/vfs_implementation.c + ${LRC}/file/file_path.c + ${LRC}/compat/compat_strl.c + ${LRC}/compat/fopen_utf8.c + ${LRC}/compat/compat_posix_string.c + ${LRC}/string/stdstring.c + ${LRC}/lists/string_list.c + ${LRC}/encodings/encoding_utf.c + ${LRC}/time/rtime.c + ${LRC}/rthreads/rthreads.c + ${LRC}/memmap/memalign.c + ${LRC}/hash/rhash.c + ) + + add_executable(psx-beetleoracle + ${PSX_SHARED_SOURCES} + src/stub_interpreter.c + ../generated/SCPH1001_full.c + ../generated/SCPH1001_dispatch.c + src/beetle_psx_bridge.cpp + ${LIBRETRO_COMMON_SOURCES} + ) + + target_include_directories(psx-beetleoracle PRIVATE + include + ${SDL2_INCLUDE_DIRS} + ${CMAKE_SOURCE_DIR}/../beetle-psx/libretro-common/include + ${CMAKE_SOURCE_DIR}/../beetle-psx + ) + + target_compile_definitions(psx-beetleoracle PRIVATE + PSX_NATIVE_BUILD=1 + ENABLE_BEETLE_PSX_ORACLE=1 + DEFAULT_DEBUG_PORT=4370 + ) + + target_link_libraries(psx-beetleoracle PRIVATE + "${BEETLE_LIB}" + ${SDL2_LIBRARIES} + ) + + if(WIN32 OR MINGW) + target_link_libraries(psx-beetleoracle PRIVATE ws2_32 -lpthread) + endif() + + if(MINGW) + target_link_options(psx-beetleoracle PRIVATE -Wl,--stack,67108864) + endif() + +elseif(NOT MSVC) + message(STATUS "Beetle PSX oracle: libmednafen_psx.a not found — skipping psx-beetleoracle") +endif() + +# ---- DuckStation oracle target (MSVC only) ---- +# +# Embeds DuckStation as a golden oracle for byte-level RAM comparison. +# Requires DuckStation pre-built at duckstation/build/ and SDL2 MSVC libs. +# Build with: cmake -G "Visual Studio 17 2022" -A x64 .. +# cmake --build . --target psx-dsoracle --config Release + +if(MSVC) + set(DS_ROOT "${CMAKE_SOURCE_DIR}/../duckstation") + set(DS_BUILD "${DS_ROOT}/build") + set(DS_PREBUILT "${DS_ROOT}/dep/prebuilt/windows-x64/lib") + set(DS_CORE_LIB "${DS_BUILD}/src/core/Release/core.lib") + + if(EXISTS "${DS_CORE_LIB}") + message(STATUS "DuckStation oracle: core.lib found at ${DS_CORE_LIB}") + + add_executable(psx-dsoracle + ${PSX_SHARED_SOURCES} + src/stub_interpreter.c + ../generated/SCPH1001_full.c + ../generated/SCPH1001_dispatch.c + src/duckstation_bridge.cpp + ) + + target_include_directories(psx-dsoracle PRIVATE + include + ${SDL2_INCLUDE_DIRS} + ${DS_ROOT}/src + ${DS_ROOT}/dep + ${DS_ROOT}/dep/fmt/include + ${DS_ROOT}/dep/fast_float/include + ${DS_ROOT}/dep/xxhash/include + ${DS_ROOT}/dep/rcheevos/include + ${DS_ROOT}/dep/rapidyaml/src + ${DS_ROOT}/dep/rapidyaml/ext/c4core/src + ${DS_ROOT}/dep/imgui/include + ${DS_ROOT}/dep/vulkan/include + ${DS_ROOT}/dep/reshadefx/include + ${DS_BUILD}/src/scmversion + ) + + target_compile_definitions(psx-dsoracle PRIVATE + PSX_NATIVE_BUILD=1 + ENABLE_DUCKSTATION_ORACLE=1 + DEFAULT_DEBUG_PORT=4370 + _CRT_SECURE_NO_WARNINGS + _CRT_NONSTDC_NO_DEPRECATE + WIN32_LEAN_AND_MEAN + NOMINMAX + _HAS_EXCEPTIONS=0 + _UNICODE + UNICODE + RC_CLIENT_SUPPORTS_RAINTEGRATION=1 + SDL_MAIN_HANDLED + CPUINFO_SHARED + ENABLE_RECOMPILER=1 + ENABLE_MMAP_FASTMEM=1 + ENABLE_OPENGL=1 + ENABLE_VULKAN=1 + ENABLE_SDL + FMT_EXCEPTIONS=0 + FMT_USE_RTTI=0 + FMT_CPP_LIB_FILESYSTEM=0 + XBYAK_NO_EXCEPTION=1 + XXH_STATIC_LINKING_ONLY + C4_NO_DEBUG_BREAK + PLUTOSVG_HAS_FREETYPE + ) + + target_compile_features(psx-dsoracle PRIVATE cxx_std_20) + # Match DuckStation's compiler flags: no exceptions, no RTTI, no GS. + target_compile_options(psx-dsoracle PRIVATE /utf-8 /GS- /guard:cf- /EHs-c-) + # Override CMake's default CRT for Debug config. + set_target_properties(psx-dsoracle PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" + ) + + # DuckStation libraries (built by tools/duckstation/build.sh). + target_link_libraries(psx-dsoracle PRIVATE + "${DS_BUILD}/src/core/Release/core.lib" + "${DS_BUILD}/src/common/Release/common.lib" + "${DS_BUILD}/src/util/Release/util.lib" + "${DS_BUILD}/src/scmversion/Release/scmversion.lib" + "${DS_BUILD}/dep/fmt/Release/fmt.lib" + "${DS_BUILD}/dep/xxhash/Release/xxhash.lib" + "${DS_BUILD}/dep/rapidyaml/Release/rapidyaml.lib" + "${DS_BUILD}/dep/rcheevos/Release/rcheevos.lib" + "${DS_BUILD}/dep/imgui/Release/imgui.lib" + "${DS_BUILD}/dep/libchdr/Release/libchdr.lib" + "${DS_BUILD}/dep/lzma/Release/lzma.lib" + "${DS_BUILD}/dep/minizip/Release/minizip.lib" + "${DS_BUILD}/dep/cubeb/Release/cubeb.lib" + "${DS_BUILD}/dep/cubeb/speex.dir/Release/speex.lib" + "${DS_BUILD}/dep/glad/Release/glad.lib" + "${DS_BUILD}/dep/reshadefx/Release/reshadefx.lib" + "${DS_BUILD}/dep/d3d12ma/Release/d3d12ma.lib" + ) + + # DuckStation prebuilt dependencies (cpuinfo, zlib, zstd, etc.). + target_link_libraries(psx-dsoracle PRIVATE + "${DS_PREBUILT}/cpuinfo.lib" + "${DS_PREBUILT}/zlib.lib" + "${DS_PREBUILT}/zstd.lib" + "${DS_PREBUILT}/zip.lib" + "${DS_PREBUILT}/freetype.lib" + "${DS_PREBUILT}/harfbuzz.lib" + "${DS_PREBUILT}/libpng16.lib" + "${DS_PREBUILT}/jpeg.lib" + "${DS_PREBUILT}/libwebp.lib" + "${DS_PREBUILT}/libsharpyuv.lib" + "${DS_PREBUILT}/soundtouch.lib" + "${DS_PREBUILT}/shaderc_shared.lib" + "${DS_PREBUILT}/spirv-cross-c-shared.lib" + "${DS_PREBUILT}/discord-rpc.lib" + "${DS_PREBUILT}/plutosvg.lib" + "${DS_PREBUILT}/brotlidec.lib" + "${DS_PREBUILT}/brotlicommon.lib" + ) + + # SDL2 (our display) + SDL3 (DuckStation's util.lib) + Windows system libs. + target_link_libraries(psx-dsoracle PRIVATE + ${SDL2_LIBRARIES} + "${DS_PREBUILT}/SDL3.lib" + ws2_32 winmm shlwapi advapi32 ole32 oleaut32 uuid + dxgi d3d11 d3d12 d3dcompiler dxguid + setupapi version imm32 + pathcch cfgmgr32 winhttp + onecore ksuser + delayimp + ) + target_link_options(psx-dsoracle PRIVATE + /DELAYLOAD:api-ms-win-core-path-l1-1-0.dll + ) + + # Large stack + suppress CRT mismatch when mixing Debug/Release configs. + target_link_options(psx-dsoracle PRIVATE + /STACK:67108864,67108864 + /IGNORE:4099 + /IGNORE:4286 + ) + + # Copy SDL2.dll next to output. + if(SDL2_MSVC_DIR) + add_custom_command(TARGET psx-dsoracle POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${SDL2_MSVC_DIR}/lib/x64/SDL2.dll" + "$/SDL2.dll" + ) + endif() + + else() + message(STATUS "DuckStation oracle: core.lib not found — skipping psx-dsoracle") + endif() +endif() diff --git a/runtime/include/psx_oracle_backend.h b/runtime/include/psx_oracle_backend.h new file mode 100644 index 00000000..55ea321d --- /dev/null +++ b/runtime/include/psx_oracle_backend.h @@ -0,0 +1,65 @@ +/* + * psx_oracle_backend.h -- Abstract interface for PSX golden-oracle backends. + * + * A backend wraps a reference PS1 emulator (DuckStation, Mednafen, etc.) + * and exposes frame-step + memory-peek operations so the debug server can + * compare recomp state against ground truth byte-by-byte. + * + * Pattern follows snesrecomp/runner/src/snes_oracle_backend.h. + */ +#ifndef PSX_ORACLE_BACKEND_H +#define PSX_ORACLE_BACKEND_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* R3000A register snapshot for cross-runtime comparison. */ +typedef struct { + uint32_t gpr[32]; + uint32_t pc; + uint32_t hi, lo; + uint32_t cop0_sr; + uint32_t cop0_cause; + uint32_t cop0_epc; +} PsxCpuRegs; + +/* Abstract backend. One instance per emulator. */ +typedef struct psx_oracle_backend { + const char *name; /* "duckstation" */ + + int (*init)(const char *bios_path); /* 0 = success */ + void (*shutdown)(void); + int (*is_loaded)(void); /* 1 if booted */ + + void (*run_frame)(uint16_t pad1_buttons); /* advance one VBlank */ + uint32_t (*get_frame_count)(void); + + /* Memory access (physical addresses, no MMIO side effects). */ + void (*get_ram)(uint8_t *out_2mb); /* copy 2 MB main RAM */ + void (*get_vram)(uint16_t *out_1mb); /* copy 1 MB GPU VRAM (1024x512 x 16bpp) */ + void (*get_scratchpad)(uint8_t *out_1kb); /* copy 1 KB scratchpad */ + uint8_t (*read_byte)(uint32_t phys_addr); /* handles RAM, scratchpad, BIOS */ + uint32_t (*read_word)(uint32_t phys_addr); + + /* CPU state. */ + void (*get_cpu_regs)(PsxCpuRegs *out); + + /* Sync: step oracle until ram[addr & 0x1FFFFF] == val, max_frames limit. */ + int (*sync_to_state)(uint32_t addr, uint32_t val, uint16_t pad, int max_frames); +} psx_oracle_backend_t; + +/* Active backend (NULL when oracle is disabled or not yet initialized). */ +extern const psx_oracle_backend_t *g_psx_oracle; + +/* Convenience wrappers called from main.cpp / debug_server.c. */ +int psx_oracle_init(const char *bios_path); /* selects + inits default backend */ +void psx_oracle_shutdown(void); +void psx_oracle_run_frame(uint16_t pad1_buttons); + +#ifdef __cplusplus +} +#endif +#endif /* PSX_ORACLE_BACKEND_H */ diff --git a/runtime/src/beetle_psx_bridge.cpp b/runtime/src/beetle_psx_bridge.cpp new file mode 100644 index 00000000..35a75007 --- /dev/null +++ b/runtime/src/beetle_psx_bridge.cpp @@ -0,0 +1,420 @@ +/* + * beetle_psx_bridge.cpp — Beetle PSX (mednafen-psx) libretro oracle backend. + * + * Loads beetle-psx as a statically-linked oracle emulator alongside the + * recompiled BIOS code, exposing a psx_oracle_backend_t that the generic + * psx_oracle_cmds.c dispatches through. + * + * Only compiled when ENABLE_BEETLE_PSX_ORACLE is defined. + * + * Pattern mirrors snesrecomp/runner/src/snes9x_bridge.cpp. + */ + +#ifdef ENABLE_BEETLE_PSX_ORACLE + +#include "psx_oracle_backend.h" + +#include +#include +#include +#include +#include + +/* Beetle PSX libretro API. */ +#include "libretro.h" + +/* Beetle PSX internals — access CPU registers, VRAM, scratchpad directly. + * These are the same globals beetle uses internally. */ +#include "mednafen/psx/psx.h" +#include "mednafen/psx/cpu.h" +#include "mednafen/psx/gpu.h" + +namespace { + +/* ---- State ---- */ +uint32_t s_framebuf[1024 * 512] = {}; +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; + +/* Per-frame RAM snapshot for delta tracking. */ +static uint8_t s_ram_before[2 * 1024 * 1024]; + +/* Pointer to system directory (where BIOS lives). */ +static char s_system_dir[4096] = "."; + +/* ---- Libretro callbacks ---- */ + +void retro_video_refresh(const void *data, unsigned width, unsigned height, size_t pitch) { + s_frame_width = width; + s_frame_height = height; + /* We don't need the framebuffer — oracle is headless. */ +} + +void retro_audio_sample(int16_t left, int16_t right) { + /* Discard audio. */ +} + +size_t retro_audio_sample_batch(const int16_t *data, size_t frames) { + return frames; /* Discard audio. */ +} + +void retro_input_poll(void) { + /* Nothing — we inject pad state via s_joypad. */ +} + +int16_t retro_input_state(unsigned port, unsigned device, unsigned index, unsigned id) { + if (port != 0 || device != RETRO_DEVICE_JOYPAD) + return 0; + /* Map libretro button ID → PS1 pad bit (active-low). + * Libretro: B=0 Y=1 Sel=2 Sta=3 U=4 D=5 L=6 R=7 A=8 X=9 L1=10 R1=11 L2=12 R2=13 L3=14 R3=15 + * PS1 pad: Sel=0 L3=1 R3=2 Sta=3 U=4 R=5 D=6 L=7 L2=8 R2=9 L1=10 R1=11 Tri=12 Cir=13 X=14 Sq=15 */ + static const int lr_to_psx[16] = { + 14, /* B(Cross) → PS1 bit 14 */ + 15, /* Y(Square) → PS1 bit 15 */ + 0, /* Select → PS1 bit 0 */ + 3, /* Start → PS1 bit 3 */ + 4, /* Up → PS1 bit 4 */ + 6, /* Down → PS1 bit 6 */ + 7, /* Left → PS1 bit 7 */ + 5, /* Right → PS1 bit 5 */ + 13, /* A(Circle) → PS1 bit 13 */ + 12, /* X(Triangle)→ PS1 bit 12 */ + 10, /* L1 → PS1 bit 10 */ + 11, /* R1 → PS1 bit 11 */ + 8, /* L2 → PS1 bit 8 */ + 9, /* R2 → PS1 bit 9 */ + 1, /* L3 → PS1 bit 1 */ + 2, /* R3 → PS1 bit 2 */ + }; + if (id >= 16) return 0; + int psx_bit = lr_to_psx[id]; + /* PS1 active-low: bit=0 means pressed. Libretro: return 1 for pressed. */ + return ((s_joypad >> psx_bit) & 1) ? 0 : 1; +} + +static void retro_log_printf(enum retro_log_level level, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::vfprintf(stderr, fmt, ap); + va_end(ap); +} + +bool retro_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: { + /* Accept whatever format beetle wants. */ + return true; + } + case RETRO_ENVIRONMENT_GET_VARIABLE: { + struct retro_variable *var = (struct retro_variable *)data; + /* Configure for interpreter (no dynarec) and no skip BIOS. */ + 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; } + /* Memory cards: enable both slots, use mednafen method + * so the BIOS sees cards present during SIO probing. */ + 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; } + /* CD access: sync to avoid async callback issues with dummy disc */ + 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 = retro_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; + default: + return false; + } +} + +} /* anonymous namespace */ + +/* ---- Public C API ---- */ + +static int beetle_init(const char *bios_path) { + if (s_loaded) return 0; + + /* Set system directory to the directory containing the BIOS. + * Beetle PSX looks for scph5501.bin (etc.) in the system dir. */ + strncpy(s_system_dir, bios_path, sizeof(s_system_dir) - 1); + /* Strip filename, keep directory. */ + 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(retro_environment); + retro_set_video_refresh(retro_video_refresh); + retro_set_audio_sample(retro_audio_sample); + retro_set_audio_sample_batch(retro_audio_sample_batch); + retro_set_input_poll(retro_input_poll); + retro_set_input_state(retro_input_state); + + std::fprintf(stderr, "[beetle-psx] retro_init...\n"); std::fflush(stderr); + retro_init(); + + /* Beetle PSX requires a disc image path even for BIOS-only boot. + * Use a dummy CUE that points to a minimal BIN. The BIOS will boot + * to the shell when it can't read a valid PS1 disc. */ + char dummy_cue[4096]; + snprintf(dummy_cue, sizeof(dummy_cue), "%s/dummy.cue", s_system_dir); + std::fprintf(stderr, "[beetle-psx] retro_load_game(%s)...\n", dummy_cue); std::fflush(stderr); + + struct retro_game_info info; + memset(&info, 0, sizeof(info)); + info.path = dummy_cue; + + if (!retro_load_game(&info)) { + std::fprintf(stderr, "[beetle-psx] retro_load_game failed\n"); + retro_deinit(); + return -1; + } + std::fprintf(stderr, "[beetle-psx] retro_load_game succeeded\n"); std::fflush(stderr); + + s_loaded = true; + s_frame_count = 0; + std::fprintf(stderr, "[beetle-psx] Oracle backend loaded (system_dir=%s)\n", s_system_dir); + return 0; +} + +static void beetle_shutdown(void) { + if (!s_loaded) return; + retro_unload_game(); + retro_deinit(); + s_loaded = false; +} + +static int beetle_is_loaded(void) { + return s_loaded ? 1 : 0; +} + +static void beetle_run_frame(uint16_t pad1_buttons) { + if (!s_loaded) return; + s_joypad = pad1_buttons; + + /* Snapshot RAM before frame for delta tracking. */ + 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_size = ram_size < sizeof(s_ram_before) ? ram_size : sizeof(s_ram_before); + memcpy(s_ram_before, ram, copy_size); + } + + s_frame_count++; + retro_run(); +} + +static uint32_t beetle_get_frame_count(void) { + return s_frame_count; +} + +static 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_size = ram_size < (2 * 1024 * 1024) ? ram_size : (2 * 1024 * 1024); + memcpy(out_2mb, ram, copy_size); + if (copy_size < 2 * 1024 * 1024) + memset(out_2mb + copy_size, 0, 2 * 1024 * 1024 - copy_size); + } else { + memset(out_2mb, 0, 2 * 1024 * 1024); + } +} + +static uint8_t beetle_read_byte(uint32_t phys) { + /* Main RAM: 0x00000000-0x001FFFFF (2MB, mirrored at 0x00200000 intervals) */ + if (phys < 0x00800000u) { + uint32_t offset = phys & 0x1FFFFF; + if (MainRAM) return MainRAM->data8[offset]; + } + /* Scratchpad: 0x1F800000-0x1F8003FF (1KB) */ + if (phys >= 0x1F800000u && phys < 0x1F800400u) { + if (ScratchRAM) return ScratchRAM->data8[phys & 0x3FF]; + } + /* BIOS ROM: 0x1FC00000-0x1FC7FFFF (512KB) */ + if (phys >= 0x1FC00000u && phys < 0x1FC80000u) { + if (BIOSROM) return BIOSROM->data8[phys & 0x7FFFF]; + } + return 0; +} + +static 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; +} + +static 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); +} + +static void beetle_get_scratchpad(uint8_t *out_1kb) { + if (ScratchRAM) + memcpy(out_1kb, ScratchRAM->data8, 1024); + else + memset(out_1kb, 0, 1024); +} + +static int beetle_sync_to_state(uint32_t addr, uint32_t val, uint16_t pad, int max_frames) { + if (!s_loaded) return -1; + uint32_t offset = addr & 0x1FFFFF; + for (int i = 0; i < max_frames; i++) { + beetle_run_frame(pad); + if (MainRAM) { + uint32_t cur; + memcpy(&cur, MainRAM->data8 + offset, 4); + if (cur == val) return i + 1; /* frames needed */ + } + } + return -1; /* timeout */ +} + +/* Exposed for emu_sync_press: sync to state with no buttons, then + * run N more frames with pad pressed. Used to navigate the shell + * menu where input must arrive at exactly the right state. */ +extern "C" int beetle_sync_then_press(uint32_t wait_addr, uint32_t wait_val, + uint32_t goal_addr, uint32_t goal_val, + uint16_t pad, int wait_max, int press_max) { + if (!s_loaded) return -1; + /* Phase 1: reach wait state with no buttons. */ + int r1 = beetle_sync_to_state(wait_addr, wait_val, 0xFFFF, wait_max); + if (r1 < 0) return -1; + /* Phase 2: run with pad pressed until goal state. + * Alternate between released (0xFFFF) and pressed (pad) to generate + * button-edge transitions that the shell's input polling detects. */ + uint32_t goal_off = goal_addr & 0x1FFFFF; + for (int i = 0; i < press_max; i++) { + /* 2-frame cycle: release then press. This creates a clean + * button-down edge every 2 frames. */ + uint16_t frame_pad = (i % 2 == 0) ? 0xFFFF : pad; + beetle_run_frame(frame_pad); + if (MainRAM) { + uint32_t cur; + memcpy(&cur, MainRAM->data8 + goal_off, 4); + if (cur == goal_val) return r1 + i + 1; + } + } + return -1; +} + +static void beetle_get_cpu_regs(PsxCpuRegs *out) { + memset(out, 0, sizeof(*out)); + if (!s_loaded || !PSX_CPU) return; + for (int i = 0; i < 32; i++) + out->gpr[i] = PSX_CPU->GetRegister(PS_CPU::GSREG_GPR + i, NULL, 0); + out->pc = PSX_CPU->GetRegister(PS_CPU::GSREG_PC, NULL, 0); + out->hi = PSX_CPU->GetRegister(PS_CPU::GSREG_HI, NULL, 0); + out->lo = PSX_CPU->GetRegister(PS_CPU::GSREG_LO, NULL, 0); + out->cop0_sr = PSX_CPU->GetRegister(PS_CPU::GSREG_SR, NULL, 0); + out->cop0_cause = PSX_CPU->GetRegister(PS_CPU::GSREG_CAUSE, NULL, 0); + out->cop0_epc = PSX_CPU->GetRegister(PS_CPU::GSREG_EPC, NULL, 0); +} + +/* ---- Backend instance ---- */ + +static const psx_oracle_backend_t s_beetle_backend = { + "beetle-psx", + beetle_init, + beetle_shutdown, + beetle_is_loaded, + beetle_run_frame, + beetle_get_frame_count, + beetle_get_ram, + beetle_get_vram, + beetle_get_scratchpad, + beetle_read_byte, + beetle_read_word, + beetle_get_cpu_regs, + beetle_sync_to_state, +}; + +/* ---- Convenience wrappers (replaces psx_oracle_stub.c when enabled) ---- */ + +const psx_oracle_backend_t *g_psx_oracle = nullptr; + +extern "C" int psx_oracle_init(const char *bios_path) { + g_psx_oracle = &s_beetle_backend; + return g_psx_oracle->init(bios_path); +} + +extern "C" void psx_oracle_shutdown(void) { + if (g_psx_oracle) { + g_psx_oracle->shutdown(); + g_psx_oracle = nullptr; + } +} + +extern "C" void psx_oracle_run_frame(uint16_t pad1_buttons) { + if (g_psx_oracle && g_psx_oracle->is_loaded()) + g_psx_oracle->run_frame(pad1_buttons); +} + +#endif /* ENABLE_BEETLE_PSX_ORACLE */ diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 06fb1d97..7f2ae2d3 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -583,16 +583,34 @@ static void handle_gte_state(int id, const char *json) static void handle_sio_state(int id, const char *json) { (void)json; + extern int sio_get_mc_probe_count(void); + extern int sio_get_mc_ack_count(void); + extern int sio_get_mc_cmd_count(void); + extern int sio_get_tx_writes(void); + extern int sio_get_tx_gated(void); + extern uint16_t sio_get_last_ctrl_on_tx(void); send_fmt("{\"id\":%d,\"ok\":true," "\"sio_stat\":\"0x%04X\"," "\"sio_ctrl\":\"0x%04X\"," "\"sio_rx\":\"0x%02X\"," - "\"pad_buttons\":\"0x%04X\"}", + "\"pad_buttons\":\"0x%04X\"," + "\"mc_probes\":%d," + "\"mc_acks\":%d," + "\"mc_cmds\":%d," + "\"tx_writes\":%d," + "\"tx_gated\":%d," + "\"last_ctrl_on_tx\":\"0x%04X\"}", id, (uint16_t)sio_read(0x1F801044), (uint16_t)sio_read(0x1F80104A), (uint8_t)sio_read(0x1F801040), - sio_get_pad_buttons()); + sio_get_pad_buttons(), + sio_get_mc_probe_count(), + sio_get_mc_ack_count(), + sio_get_mc_cmd_count(), + sio_get_tx_writes(), + sio_get_tx_gated(), + sio_get_last_ctrl_on_tx()); } static void handle_watch(int id, const char *json) @@ -1422,6 +1440,14 @@ static void process_command(const char *line) } } + /* Oracle commands (find_first_divergence, emu_read_ram, etc.) */ +#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE) + { + extern int psx_oracle_handle_cmd(const char *cmd, int id, const char *json); + if (psx_oracle_handle_cmd(cmd, id, line)) return; + } +#endif + send_err(id, "unknown command"); } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 34e3b158..cf545b90 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -13,6 +13,9 @@ #include "spu.h" #include "memcard.h" #include "debug_server.h" +#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE) +#include "psx_oracle_backend.h" +#endif #include #include #include @@ -117,11 +120,19 @@ static void sdl_vblank_present(void) { /* 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) { - sio_set_pad_state((uint16_t)override); + 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); + + /* Advance oracle emulator one frame with the same input. */ +#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE) + psx_oracle_run_frame(pad_buttons_this_frame); +#endif GpuDisplayInfo di; gpu_get_display_info(&di); @@ -154,6 +165,8 @@ 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"; if (argc > 1) bios_path = argv[1]; @@ -216,6 +229,18 @@ int main(int argc, char** argv) { /* Register vblank presentation callback. */ gpu_set_vblank_callback(sdl_vblank_present); + /* Initialize golden oracle (DuckStation) if enabled. */ +#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE) + { + int orc = psx_oracle_init(bios_path); + if (orc != 0) { + std::fprintf(stderr, "WARNING: oracle init failed (code %d) — continuing without oracle\n", orc); + } else { + std::fprintf(stdout, "psxrecomp-v4: DuckStation oracle initialized\n"); + } + } +#endif + /* Initialize CPU state. */ CPUState cpu; std::memset(&cpu, 0, sizeof(cpu)); diff --git a/runtime/src/memory.c b/runtime/src/memory.c index f0a3db27..5dd606dc 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -28,6 +28,9 @@ static uint8_t ram[RAM_SIZE]; static uint8_t scratchpad[SCRATCHPAD_SIZE]; static uint8_t bios_rom[BIOS_ROM_SIZE]; +/* Expose RAM pointer for oracle comparison (find_first_divergence). */ +uint8_t *memory_get_ram_ptr(void) { return ram; } + /* Memory control registers: 0x1F801000..0x1F80103F (16 words) + 0x1F801060 (RAM size). * Includes expansion base/size, COM_DELAY, SPU_DELAY, CDROM_DELAY etc. */ static uint32_t mem_ctrl[16]; /* indices 0..15 → addresses 0x1F801000..0x1F80103C */ diff --git a/runtime/src/psx_oracle_cmds.c b/runtime/src/psx_oracle_cmds.c new file mode 100644 index 00000000..131ab0e9 --- /dev/null +++ b/runtime/src/psx_oracle_cmds.c @@ -0,0 +1,356 @@ +/* + * psx_oracle_cmds.c -- TCP debug server commands for golden-oracle comparison. + * + * Handles emu_* and find_first_divergence commands. + * Compiled into the runtime only when ENABLE_DUCKSTATION_ORACLE is defined. + * + * Pattern follows snesrecomp/runner/src/emu_oracle_cmds.c. + */ + +#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE) + +#include "psx_oracle_backend.h" +#include +#include +#include +#include + +/* ---- Helpers (same as debug_server.c) ---- */ + +static int json_get_str(const char *json, const char *key, char *out, int outsz) { + char pat[64]; + snprintf(pat, sizeof(pat), "\"%s\"", key); + const char *p = strstr(json, pat); + if (!p) return 0; + p += strlen(pat); + while (*p == ' ' || *p == ':' || *p == '\t') p++; + if (*p == '"') { + p++; + int i = 0; + while (*p && *p != '"' && i < outsz - 1) out[i++] = *p++; + out[i] = '\0'; + return 1; + } + /* Unquoted (number). */ + int i = 0; + while (*p && *p != ',' && *p != '}' && *p != ' ' && i < outsz - 1) out[i++] = *p++; + out[i] = '\0'; + return 1; +} + +static uint32_t hex_to_u32(const char *s) { + return (uint32_t)strtoul(s, NULL, 0); +} + +static int json_get_int(const char *json, const char *key, int defval) { + char buf[32]; + if (!json_get_str(json, key, buf, sizeof(buf))) return defval; + return atoi(buf); +} + +/* ---- Send helpers (forward-declared from debug_server.c) ---- */ +extern void debug_server_send_fmt(const char *fmt, ...); +#define send_fmt debug_server_send_fmt + +static void send_ok(int id) { + send_fmt("{\"id\":%d,\"ok\":true}\n", id); +} +static void send_err(int id, const char *msg) { + send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"%s\"}\n", id, msg); +} + +/* ---- RAM pointer from memory.c ---- */ +extern uint8_t *memory_get_ram_ptr(void); + +/* ---- Command handlers ---- */ + +static void h_emu_is_loaded(int id, const char *json) { + (void)json; + int loaded = g_psx_oracle && g_psx_oracle->is_loaded(); + send_fmt("{\"id\":%d,\"ok\":true,\"loaded\":%s}\n", + id, loaded ? "true" : "false"); +} + +static void h_emu_read_ram(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded()) { + send_err(id, "oracle not loaded"); return; + } + char abuf[32] = {0}; + json_get_str(json, "addr", abuf, sizeof(abuf)); + uint32_t addr = hex_to_u32(abuf); + int len = json_get_int(json, "len", 4); + if (len < 1) len = 1; + if (len > 4096) len = 4096; + + /* Physical mask. */ + uint32_t phys = addr & 0x1FFFFFFFu; + + /* Read from oracle byte by byte. */ + char *hex = (char *)malloc((size_t)(len * 2 + 1)); + if (!hex) { send_err(id, "alloc"); return; } + for (int i = 0; i < len; i++) { + uint8_t b = g_psx_oracle->read_byte(phys + (uint32_t)i); + snprintf(hex + i * 2, 3, "%02x", b); + } + send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"%s\"}\n", + id, addr, len, hex); + free(hex); +} + +static void h_emu_cpu_regs(int id, const char *json) { + (void)json; + if (!g_psx_oracle || !g_psx_oracle->is_loaded()) { + send_err(id, "oracle not loaded"); return; + } + PsxCpuRegs regs; + g_psx_oracle->get_cpu_regs(®s); + + char *buf = (char *)malloc(4096); + if (!buf) { send_err(id, "alloc"); return; } + int pos = snprintf(buf, 4096, + "{\"id\":%d,\"ok\":true," + "\"pc\":\"0x%08X\",\"hi\":\"0x%08X\",\"lo\":\"0x%08X\"," + "\"sr\":\"0x%08X\",\"cause\":\"0x%08X\",\"epc\":\"0x%08X\"," + "\"gpr\":[", + id, regs.pc, regs.hi, regs.lo, + regs.cop0_sr, regs.cop0_cause, regs.cop0_epc); + for (int i = 0; i < 32; i++) { + if (i) buf[pos++] = ','; + pos += snprintf(buf + pos, 4096 - pos, "\"0x%08X\"", regs.gpr[i]); + } + pos += snprintf(buf + pos, 4096 - pos, "]}\n"); + send_fmt("%s", buf); + free(buf); +} + +static void h_emu_step(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded()) { + send_err(id, "oracle not loaded"); return; + } + int n = json_get_int(json, "frames", 1); + if (n < 1) n = 1; + if (n > 600) n = 600; + for (int i = 0; i < n; i++) { + g_psx_oracle->run_frame(0xFFFF); /* no buttons */ + } + send_fmt("{\"id\":%d,\"ok\":true,\"frames_stepped\":%d,\"oracle_frame\":%u}\n", + id, n, g_psx_oracle->get_frame_count()); +} + +static void h_find_first_divergence(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded()) { + send_err(id, "oracle not loaded"); return; + } + + char buf[32]; + uint32_t lo = 0, hi = 0x1FFFFF; + int context = 16; + if (json_get_str(json, "lo", buf, sizeof(buf))) lo = hex_to_u32(buf); + if (json_get_str(json, "hi", buf, sizeof(buf))) hi = hex_to_u32(buf); + context = json_get_int(json, "context", 16); + if (context < 0) context = 0; + if (context > 256) context = 256; + if (hi > 0x1FFFFF) hi = 0x1FFFFF; + if (lo > hi) lo = hi; + + /* Snapshot oracle RAM. */ + static uint8_t oracle_ram[0x200000]; + g_psx_oracle->get_ram(oracle_ram); + + const uint8_t *recomp_ram = memory_get_ram_ptr(); + + int32_t first_diff = -1; + int diff_count = 0; + for (uint32_t i = lo; i <= hi; i++) { + if (recomp_ram[i] != oracle_ram[i]) { + if (first_diff < 0) first_diff = (int32_t)i; + diff_count++; + } + } + + if (first_diff < 0) { + send_fmt("{\"id\":%d,\"ok\":true,\"match\":true,\"bytes_scanned\":%u}\n", + id, hi - lo + 1); + return; + } + + /* Build context window. */ + uint32_t ctx_lo = (uint32_t)first_diff >= (uint32_t)context + ? (uint32_t)first_diff - (uint32_t)context : lo; + uint32_t ctx_hi = (uint32_t)first_diff + (uint32_t)context; + if (ctx_hi > hi) ctx_hi = hi; + if (ctx_lo < lo) ctx_lo = lo; + + /* Emit context bytes. */ + int ctx_len = (int)(ctx_hi - ctx_lo + 1); + char *rbuf = (char *)malloc((size_t)(ctx_len * 2 + 1)); + char *obuf = (char *)malloc((size_t)(ctx_len * 2 + 1)); + if (!rbuf || !obuf) { free(rbuf); free(obuf); send_err(id, "alloc"); return; } + for (int i = 0; i < ctx_len; i++) { + snprintf(rbuf + i * 2, 3, "%02x", recomp_ram[ctx_lo + i]); + snprintf(obuf + i * 2, 3, "%02x", oracle_ram[ctx_lo + i]); + } + + send_fmt("{\"id\":%d,\"ok\":true,\"match\":false," + "\"first_diff\":\"0x%08X\"," + "\"recomp_val\":\"0x%02X\",\"oracle_val\":\"0x%02X\"," + "\"diff_count\":%d,\"bytes_scanned\":%u," + "\"ctx_lo\":\"0x%08X\",\"ctx_hi\":\"0x%08X\"," + "\"recomp_ctx\":\"%s\",\"oracle_ctx\":\"%s\"}\n", + id, (uint32_t)first_diff, + recomp_ram[first_diff], oracle_ram[first_diff], + diff_count, hi - lo + 1, + ctx_lo, ctx_hi, rbuf, obuf); + free(rbuf); + free(obuf); +} + +static void h_emu_ram_delta(int id, const char *json) { + /* TODO: implement once bridge has per-frame snapshot */ + (void)json; + send_err(id, "not yet implemented"); +} + +static void h_emu_sync(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded() || !g_psx_oracle->sync_to_state) { + send_err(id, "oracle not loaded or sync not supported"); return; + } + char buf[32]; + uint32_t addr = 0, val = 0; + uint16_t pad = 0xFFFF; /* no buttons by default */ + int max_frames = 1000; + if (json_get_str(json, "addr", buf, sizeof(buf))) addr = hex_to_u32(buf); + if (json_get_str(json, "val", buf, sizeof(buf))) val = hex_to_u32(buf); + if (json_get_str(json, "pad", buf, sizeof(buf))) pad = (uint16_t)hex_to_u32(buf); + max_frames = json_get_int(json, "max_frames", 1000); + if (max_frames < 1) max_frames = 1; + if (max_frames > 10000) max_frames = 10000; + + int result = g_psx_oracle->sync_to_state(addr, val, pad, max_frames); + if (result > 0) { + send_fmt("{\"id\":%d,\"ok\":true,\"synced\":true,\"frames_needed\":%d,\"oracle_frame\":%u}\n", + id, result, g_psx_oracle->get_frame_count()); + } else { + send_fmt("{\"id\":%d,\"ok\":true,\"synced\":false,\"max_frames\":%d,\"oracle_frame\":%u}\n", + id, max_frames, g_psx_oracle->get_frame_count()); + } +} + +static void h_emu_sync_press(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded()) { + send_err(id, "oracle not loaded"); return; + } + extern int beetle_sync_then_press(uint32_t, uint32_t, uint32_t, uint32_t, + uint16_t, int, int); + char buf[32]; + uint32_t wait_addr = 0, wait_val = 0, goal_addr = 0, goal_val = 0; + uint16_t pad = 0xBFFF; + int wait_max = 2000, press_max = 500; + if (json_get_str(json, "wait_addr", buf, sizeof(buf))) wait_addr = hex_to_u32(buf); + if (json_get_str(json, "wait_val", buf, sizeof(buf))) wait_val = hex_to_u32(buf); + if (json_get_str(json, "goal_addr", buf, sizeof(buf))) goal_addr = hex_to_u32(buf); + if (json_get_str(json, "goal_val", buf, sizeof(buf))) goal_val = hex_to_u32(buf); + if (json_get_str(json, "pad", buf, sizeof(buf))) pad = (uint16_t)hex_to_u32(buf); + wait_max = json_get_int(json, "wait_max", 2000); + press_max = json_get_int(json, "press_max", 500); + + int result = beetle_sync_then_press(wait_addr, wait_val, goal_addr, goal_val, + pad, wait_max, press_max); + if (result > 0) { + send_fmt("{\"id\":%d,\"ok\":true,\"synced\":true,\"frames_needed\":%d,\"oracle_frame\":%u}\n", + id, result, g_psx_oracle->get_frame_count()); + } else { + send_fmt("{\"id\":%d,\"ok\":true,\"synced\":false,\"oracle_frame\":%u}\n", + id, g_psx_oracle->get_frame_count()); + } +} + +static void h_emu_read_vram(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded() || !g_psx_oracle->get_vram) { + send_err(id, "oracle not loaded or VRAM not supported"); return; + } + /* Read a rectangular region of VRAM as hex (16bpp pixels). */ + int x = json_get_int(json, "x", 0); + int y = json_get_int(json, "y", 0); + int w = json_get_int(json, "w", 16); + int h = json_get_int(json, "h", 1); + if (w < 1) w = 1; if (w > 1024) w = 1024; + if (h < 1) h = 1; if (h > 512) h = 512; + if (x < 0) x = 0; if (x + w > 1024) w = 1024 - x; + if (y < 0) y = 0; if (y + h > 512) h = 512 - y; + + static uint16_t vram[1024 * 512]; + g_psx_oracle->get_vram(vram); + + int pixels = w * h; + if (pixels > 4096) { send_err(id, "region too large (max 4096 pixels)"); return; } + char *hex = (char *)malloc((size_t)(pixels * 4 + 1)); + if (!hex) { send_err(id, "alloc"); return; } + int pos = 0; + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + uint16_t px = vram[(y + row) * 1024 + (x + col)]; + pos += snprintf(hex + pos, 5, "%04x", px); + } + } + send_fmt("{\"id\":%d,\"ok\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"hex\":\"%s\"}\n", + id, x, y, w, h, hex); + free(hex); +} + +static void h_emu_read_scratchpad(int id, const char *json) { + if (!g_psx_oracle || !g_psx_oracle->is_loaded() || !g_psx_oracle->get_scratchpad) { + send_err(id, "oracle not loaded"); return; + } + uint8_t scratch[1024]; + g_psx_oracle->get_scratchpad(scratch); + char buf[32]; int off = 0, len = 1024; + if (json_get_str(json, "offset", buf, sizeof(buf))) off = (int)hex_to_u32(buf); + len = json_get_int(json, "len", 1024); + if (off < 0) off = 0; if (off >= 1024) off = 0; + if (len < 1) len = 1; if (off + len > 1024) len = 1024 - off; + char *hex = (char *)malloc((size_t)(len * 2 + 1)); + if (!hex) { send_err(id, "alloc"); return; } + for (int i = 0; i < len; i++) + snprintf(hex + i * 2, 3, "%02x", scratch[off + i]); + send_fmt("{\"id\":%d,\"ok\":true,\"offset\":%d,\"len\":%d,\"hex\":\"%s\"}\n", + id, off, len, hex); + free(hex); +} + +/* ---- Command dispatch ---- */ + +typedef struct { + const char *name; + void (*handler)(int id, const char *json); +} OracleCmd; + +static const OracleCmd s_oracle_cmds[] = { + { "emu_is_loaded", h_emu_is_loaded }, + { "emu_read_ram", h_emu_read_ram }, + { "emu_read_vram", h_emu_read_vram }, + { "emu_read_scratchpad", h_emu_read_scratchpad }, + { "emu_cpu_regs", h_emu_cpu_regs }, + { "emu_step", h_emu_step }, + { "emu_sync", h_emu_sync }, + { "emu_sync_press", h_emu_sync_press }, + { "emu_ram_delta", h_emu_ram_delta }, + { "find_first_divergence", h_find_first_divergence }, + { NULL, NULL } +}; + +/* + * Called from debug_server.c process_command() before the "unknown" fallback. + * Returns 1 if the command was handled, 0 otherwise. + */ +int psx_oracle_handle_cmd(const char *cmd, int id, const char *json) { + for (const OracleCmd *c = s_oracle_cmds; c->name; c++) { + if (strcmp(cmd, c->name) == 0) { + c->handler(id, json); + return 1; + } + } + return 0; +} + +#endif /* ENABLE_DUCKSTATION_ORACLE || ENABLE_BEETLE_PSX_ORACLE */ diff --git a/runtime/src/psx_oracle_stub.c b/runtime/src/psx_oracle_stub.c new file mode 100644 index 00000000..3d64bf5b --- /dev/null +++ b/runtime/src/psx_oracle_stub.c @@ -0,0 +1,25 @@ +/* + * psx_oracle_stub.c -- Stub oracle backend for builds without DuckStation. + * + * When ENABLE_DUCKSTATION_ORACLE is NOT defined, provide the g_psx_oracle + * global and convenience wrappers as no-ops so the code compiles cleanly. + */ + +#if !defined(ENABLE_DUCKSTATION_ORACLE) && !defined(ENABLE_BEETLE_PSX_ORACLE) + +#include "psx_oracle_backend.h" + +const psx_oracle_backend_t *g_psx_oracle = 0; + +int psx_oracle_init(const char *bios_path) { + (void)bios_path; + return -1; /* no backend available */ +} + +void psx_oracle_shutdown(void) {} + +void psx_oracle_run_frame(uint16_t pad1_buttons) { + (void)pad1_buttons; +} + +#endif /* !ENABLE_DUCKSTATION_ORACLE && !ENABLE_BEETLE_PSX_ORACLE */ diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 7cdd670a..0d4f1a0e 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -95,6 +95,21 @@ typedef enum { static ActiveDevice active_device = DEV_NONE; +/* Card probe diagnostic counters */ +static int sio_mc_probe_count = 0; /* times 0x81 written to TX */ +static int sio_mc_ack_count = 0; /* times card sent ACK */ +static int sio_mc_cmd_count = 0; /* times card reached CMD state */ +static int sio_tx_writes = 0; /* ANY write to SIO_TX_DATA */ +static int sio_tx_gated = 0; /* writes gated by missing TX_EN */ +static uint16_t sio_last_ctrl_on_tx = 0; /* CTRL at last TX write */ + +int sio_get_mc_probe_count(void) { return sio_mc_probe_count; } +int sio_get_mc_ack_count(void) { return sio_mc_ack_count; } +int sio_get_mc_cmd_count(void) { return sio_mc_cmd_count; } +int sio_get_tx_writes(void) { return sio_tx_writes; } +int sio_get_tx_gated(void) { return sio_tx_gated; } +uint16_t sio_get_last_ctrl_on_tx(void) { return sio_last_ctrl_on_tx; } + /* Delayed IRQ mechanism. * * On real PS1, each SIO byte transfer takes BAUD*8 cycles (~1088 for memcard) @@ -223,6 +238,7 @@ static void mc_process_byte(uint8_t tx_byte) { mc_state = MC_CMD; sio_rx_data = 0xFF; sio_stat |= SIO_STAT_ACK; + sio_mc_ack_count++; } else { sio_rx_data = 0xFF; } @@ -230,6 +246,7 @@ static void mc_process_byte(uint8_t tx_byte) { case MC_CMD: mc_cmd = tx_byte; + sio_mc_cmd_count++; if (tx_byte == 0x52 || tx_byte == 0x57 || tx_byte == 0x53) { mc_state = MC_ID1; sio_rx_data = mc_flag; @@ -415,6 +432,7 @@ static void sio_process_byte(uint8_t tx_byte) { pad_process_byte(tx_byte); } else if (tx_byte == 0x81) { active_device = DEV_MEMCARD; + sio_mc_probe_count++; mc_process_byte(tx_byte); } else { sio_rx_data = 0xFF; @@ -466,6 +484,9 @@ void sio_write(uint32_t addr, uint32_t value) { switch (addr) { case 0x1F801040: /* SIO_TX_DATA */ sio_tx_data = (uint8_t)value; + sio_tx_writes++; + sio_last_ctrl_on_tx = sio_ctrl; + if (!(sio_ctrl & SIO_CTRL_TX_EN)) sio_tx_gated++; if (sio_ctrl & SIO_CTRL_TX_EN) { sio_process_byte(sio_tx_data); if ((sio_stat & SIO_STAT_ACK) && (sio_ctrl & SIO_CTRL_ACK_IRQ_EN)) { diff --git a/runtime/src/traps.c b/runtime/src/traps.c index eb794d5b..e0af0f8e 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -364,7 +364,8 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) { static int miss_count = 0; if (!miss_log) miss_log = fopen("psx_dispatch_misses.txt", "w"); if (miss_log) { - fprintf(miss_log, "0x%08X phys=0x%08X\n", addr, phys); + fprintf(miss_log, "0x%08X phys=0x%08X ra=0x%08X t9=0x%08X pc=0x%08X\n", + addr, phys, cpu->gpr[31], cpu->gpr[25], cpu->pc); fflush(miss_log); } miss_count++; diff --git a/tools/build_oracle.sh b/tools/build_oracle.sh new file mode 100644 index 00000000..ec0dd845 --- /dev/null +++ b/tools/build_oracle.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Build the psx-dsoracle target (MSVC, links DuckStation oracle). +# +# Prerequisites: +# - DuckStation built via: bash tools/duckstation/build.sh +# - MSVC 2022 installed (detected via vswhere) +# - CMake in PATH +# +# Usage: +# bash tools/build_oracle.sh +# bash tools/build_oracle.sh clean # clean + rebuild + +set -e +cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +echo "=== psxrecomp-v4: build oracle ===" + +# Check DuckStation is built. +if [ ! -f "duckstation/build/src/core/Release/core.lib" ]; then + echo "ERROR: DuckStation not built. Run: bash tools/duckstation/build.sh" + exit 1 +fi + +# Download SDL2 MSVC dev package if not present. +if [ ! -d sdl2-msvc/SDL2-* ] 2>/dev/null; then + echo "Downloading SDL2 MSVC development package..." + curl -L -o sdl2-dev.zip \ + "https://github.com/libsdl-org/SDL/releases/download/release-2.30.12/SDL2-devel-2.30.12-VC.zip" + mkdir -p sdl2-msvc + unzip -q -o sdl2-dev.zip -d sdl2-msvc/ + rm -f sdl2-dev.zip +fi + +# Configure with MSVC generator. +BUILD_DIR="runtime/build-msvc" + +if [ "$1" = "clean" ]; then + echo "Cleaning build directory..." + rm -rf "$BUILD_DIR" +fi + +if [ ! -d "$BUILD_DIR" ]; then + echo "Configuring CMake (MSVC x64)..." + cmake -S runtime -B "$BUILD_DIR" \ + -G "Visual Studio 17 2022" -A x64 +fi + +# Build psx-dsoracle target. +echo "Building psx-dsoracle..." +cmake --build "$BUILD_DIR" --target psx-dsoracle --config Release -- /m + +echo "" +echo "=== Build complete ===" +echo "Binary: $BUILD_DIR/Release/psx-dsoracle.exe" + +# Copy SDL2.dll if not already there. +if [ -f "$BUILD_DIR/Release/psx-dsoracle.exe" ]; then + echo "Oracle binary ready." +fi diff --git a/tools/oracle_diff.py b/tools/oracle_diff.py new file mode 100644 index 00000000..24b0698e --- /dev/null +++ b/tools/oracle_diff.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +oracle_diff.py -- Golden-oracle RAM comparison tool. + +Connects to both the psxrecomp runtime (port 4370) and DuckStation oracle +(port 4371) and compares RAM byte-by-byte. + +Usage: + python tools/oracle_diff.py # Full 2MB diff + python tools/oracle_diff.py 0x0000 0x8000 # Kernel RAM only + python tools/oracle_diff.py 0x7400 0x7600 -v # Card state, verbose +""" + +import socket +import json +import sys +import time +import argparse + + +def send_cmd(port, cmd_json, timeout=3.0): + """Send a JSON command to the debug server and return the parsed response.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + try: + sock.connect(("127.0.0.1", port)) + sock.sendall((json.dumps(cmd_json) + "\n").encode()) + data = b"" + while True: + chunk = sock.recv(8192) + if not chunk: + break + data += chunk + if b"\n" in data: + break + return json.loads(data.decode().strip()) + finally: + sock.close() + + +def read_ram(port, addr, length): + """Read RAM from a debug server, returns bytes.""" + resp = send_cmd(port, {"cmd": "read_ram", "addr": f"0x{addr:08X}", "len": length}) + if not resp.get("ok"): + raise RuntimeError(f"read_ram failed on port {port}: {resp}") + return bytes.fromhex(resp["hex"]) + + +def find_first_divergence(recomp_port, oracle_port, lo=0, hi=0x1FFFFF, context=16, chunk_size=4096): + """Compare RAM between recomp and oracle, return first difference.""" + first_diff = None + diff_count = 0 + total_scanned = 0 + + addr = lo + while addr <= hi: + length = min(chunk_size, hi - addr + 1) + recomp_data = read_ram(recomp_port, addr, length) + oracle_data = read_ram(oracle_port, addr, length) + + for i in range(len(recomp_data)): + total_scanned += 1 + if recomp_data[i] != oracle_data[i]: + diff_count += 1 + if first_diff is None: + first_diff = addr + i + + addr += length + + if first_diff is None: + return { + "match": True, + "bytes_scanned": total_scanned, + } + + # Read context around first difference + ctx_lo = max(lo, first_diff - context) + ctx_hi = min(hi, first_diff + context) + ctx_len = ctx_hi - ctx_lo + 1 + recomp_ctx = read_ram(recomp_port, ctx_lo, ctx_len) + oracle_ctx = read_ram(oracle_port, ctx_lo, ctx_len) + + return { + "match": False, + "first_diff": first_diff, + "recomp_val": recomp_ctx[first_diff - ctx_lo], + "oracle_val": oracle_ctx[first_diff - ctx_lo], + "diff_count": diff_count, + "bytes_scanned": total_scanned, + "ctx_lo": ctx_lo, + "ctx_hi": ctx_hi, + "recomp_ctx": recomp_ctx.hex(), + "oracle_ctx": oracle_ctx.hex(), + } + + +def print_context(result, verbose=False): + """Pretty-print a divergence result with hex context.""" + if result["match"]: + print(f"MATCH: {result['bytes_scanned']:,} bytes scanned, no differences.") + return + + addr = result["first_diff"] + print(f"DIVERGENCE at 0x{addr:08X}: recomp=0x{result['recomp_val']:02X} oracle=0x{result['oracle_val']:02X}") + print(f" Total differences: {result['diff_count']:,} / {result['bytes_scanned']:,} bytes") + + if verbose: + ctx_lo = result["ctx_lo"] + recomp = bytes.fromhex(result["recomp_ctx"]) + oracle = bytes.fromhex(result["oracle_ctx"]) + print(f"\n Context [0x{ctx_lo:08X} .. 0x{result['ctx_hi']:08X}]:") + print(f" {'Addr':>10s} {'Recomp':>6s} {'Oracle':>6s} {'Match':>5s}") + for i in range(len(recomp)): + a = ctx_lo + i + r = recomp[i] + o = oracle[i] + marker = " " if r == o else " <---" + print(f" 0x{a:08X} 0x{r:02X} 0x{o:02X} {marker}") + + +def main(): + parser = argparse.ArgumentParser(description="Oracle RAM comparison tool") + parser.add_argument("lo", nargs="?", default="0x0000", help="Start address (hex)") + parser.add_argument("hi", nargs="?", default="0x1FFFFF", help="End address (hex)") + parser.add_argument("-v", "--verbose", action="store_true", help="Show context bytes") + parser.add_argument("--recomp-port", type=int, default=4370, help="Recomp debug port") + parser.add_argument("--oracle-port", type=int, default=4371, help="Oracle debug port") + parser.add_argument("--context", type=int, default=16, help="Context bytes around first diff") + args = parser.parse_args() + + lo = int(args.lo, 0) + hi = int(args.hi, 0) + + # Verify both servers are reachable + for port, name in [(args.recomp_port, "recomp"), (args.oracle_port, "oracle")]: + try: + resp = send_cmd(port, {"cmd": "ping"}) + if resp.get("ok"): + print(f" {name} (port {port}): connected (frame {resp.get('frame', '?')})") + else: + print(f" {name} (port {port}): ping failed") + sys.exit(1) + except Exception as e: + print(f" {name} (port {port}): connection failed: {e}") + sys.exit(1) + + print(f"\nComparing RAM 0x{lo:08X}..0x{hi:08X} ({hi - lo + 1:,} bytes)...") + result = find_first_divergence(args.recomp_port, args.oracle_port, lo, hi, args.context) + print() + print_context(result, args.verbose) + + +if __name__ == "__main__": + main()