mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
feat: embed Beetle PSX libretro core as golden oracle for RAM comparison
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) <noreply@anthropic.com>
This commit is contained in:
+281
-17
@@ -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
|
||||
$<$<CXX_COMPILER_ID:MSVC>: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"
|
||||
"$<TARGET_FILE_DIR:psx-dsoracle>/SDL2.dll"
|
||||
)
|
||||
endif()
|
||||
|
||||
else()
|
||||
message(STATUS "DuckStation oracle: core.lib not found — skipping psx-dsoracle")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
@@ -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 <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdarg>
|
||||
#include <vector>
|
||||
|
||||
/* 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 */
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
+26
-1
@@ -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 <SDL.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -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));
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 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 */
|
||||
@@ -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 */
|
||||
@@ -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)) {
|
||||
|
||||
+2
-1
@@ -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++;
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user