diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9c895c1..b9870d64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,7 +139,7 @@ cmake --build recompiler/build cd recompiler/build && ctest --output-on-failure ``` -36 tests, under five seconds, no BIOS dump or disc image required. See +38 tests, under five seconds, no BIOS dump or disc image required. See [`docs/TESTING.md`](docs/TESTING.md) for running individual tests, what the suite covers, and the three known-failing tests that are deliberately not registered. diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index f2587c08..a07020a9 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -90,12 +90,30 @@ Columns: **N** = native, **D** = DuckStation oracle. | `pc_hit_clear` | | ✓² | — | Clear the last-hit record | | `quit` | ✓ | | — | Shutdown native runtime | -¹ Native `vram_peek` is the legacy name; DS calls it `read_vram`. Same semantics. -² The `pc_*` family is specific to the DS oracle: DuckStation's CPU core honours `CPU::AddBreakpointWithCallback`, while our native runtime dispatches whole recompiled functions (no mid-function PC breaks). - ---- - -## Divergence-hunt workflow +¹ Native `vram_peek` is the legacy name; DS calls it `read_vram`. Same semantics. +² The `pc_*` family is specific to the DS oracle: DuckStation's CPU core honours `CPU::AddBreakpointWithCallback`, while our native runtime dispatches whole recompiled functions (no mid-function PC breaks). + +### Boot-time write ranges + +Set `PSX_WTRACE_BOOT=lo,hi[;lo,hi...]` before launching a debug-tools build to +retain the first writes to one or more half-open RAM ranges from guest +instruction zero. Addresses may be hexadecimal or decimal; KSEG addresses are +normalized to physical addresses. For example, the Crash Bash investigation +that motivated this option can be reproduced without title-specific code: + +```powershell +$env:PSX_WTRACE_BOOT='0x000B3A80,0x000B3B00' +.\CrashBashRecomp.exe +``` + +Connect at any later point and query `wtrace_boot_stats`, +`wtrace_boot_summary`, or `wtrace_boot_dump`. Each retained entry includes the +write address/value/width, guest PC and return address, register context, frame, +and DMA channel. The option is ignored in builds made with debug tools disabled. + +--- + +## Divergence-hunt workflow When a recompiled-BIOS bug is suspected, the two servers let you find the **first** divergence instead of chasing symptoms. Standard procedure (inherited from v3's `DEBUG.md`): diff --git a/docs/BUILDING.md b/docs/BUILDING.md index e93691b0..1d09623b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -135,7 +135,7 @@ After step 1 above — no BIOS or disc needed — verify the tree is sane: cd recompiler/build && ctest --output-on-failure ``` -36 tests, under five seconds. See [`TESTING.md`](TESTING.md). +38 tests, under five seconds. See [`TESTING.md`](TESTING.md). On Windows with MSVC or plain MinGW makefiles, swap `-G Ninja` for your generator (e.g. `-G "Unix Makefiles"`); everything else is identical. diff --git a/docs/TESTING.md b/docs/TESTING.md index 57380d3c..2fe84b13 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -8,7 +8,7 @@ cmake --build recompiler/build cd recompiler/build && ctest --output-on-failure ``` -That is the whole thing. 36 tests, under 5 seconds, and it needs **no BIOS dump, +That is the whole thing. 38 tests, under 5 seconds, and it needs **no BIOS dump, no disc image, and no generated code** — a plain recompiler build is enough. This is the check to run before opening a PR. diff --git a/docs/internal/upstream/pr13-generic-boot-write-watch.md b/docs/internal/upstream/pr13-generic-boot-write-watch.md new file mode 100644 index 00000000..be8df438 --- /dev/null +++ b/docs/internal/upstream/pr13-generic-boot-write-watch.md @@ -0,0 +1,28 @@ +# PR #13 generic boot write-watch provenance + +Extracted from NyperYuhgard's PSXrecomp PR #13, commit +[`b07cb79251ecdca85aca94ba5dbfd731b67e10a7`](https://github.com/mstan/psxrecomp/commit/b07cb79251ecdca85aca94ba5dbfd731b67e10a7). + +The upstream change identified a suspicious Crash Bash write by adding an +environment-gated watcher for the hard-coded physical range +`0x000B3A80..0x000B3AFF`. This extraction preserves the useful behavior while +removing the title-specific address and stderr-only reporting: + +- `PSX_WTRACE_BOOT` accepts one or more caller-selected half-open ranges. +- Ranges feed the existing boot-pinned write trace from instruction zero. +- Retained records include PC, return address, register context, frame, width, + and DMA attribution and remain queryable later through the TCP debug server. + +The companion misaligned-dispatch diagnostics from the same source commit were +previously extracted in PR #28. + +## Reproduction note + +A verified retail Greatest Hits Crash Bash build exercised with the generic +range did write the watched address, confirming that the address and mechanism +are plausible. In that build, however, the nonzero write at physical +`0x000B3AC4` was the MIPS instruction word `0x8C65B690`, written by guest PC +`0x80048FA8` at frame 395. It did not reproduce the originally reported +`0x6766BD35` pointer value. For that reason this extraction exposes the +diagnostic generically and does not encode the unconfirmed title-specific +interpretation. diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 654419a8..52202b0f 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -263,8 +263,16 @@ target_include_directories(l2_structural_test PRIVATE ) target_link_libraries(l2_structural_test PRIVATE fmt rabbitizer) -if(BUILD_TESTING) - add_executable(cli_boot_path_test +if(BUILD_TESTING) + add_executable(debug_trace_ranges_test + ../runtime/tests/test_debug_trace_ranges.c + ../runtime/src/debug_trace_ranges.c + ) + target_include_directories(debug_trace_ranges_test PRIVATE + ../runtime/include) + add_test(NAME debug_trace_ranges_test COMMAND debug_trace_ranges_test) + + add_executable(cli_boot_path_test tests/cli_boot_path_test.cpp src/cli_boot_path.cpp ) @@ -401,8 +409,9 @@ if(BUILD_TESTING) mod_controller_override overlay_candidate_capacity overlay_decodable_fallback - overlay_init_guard - vk_build_default + overlay_init_guard + sdl3_main_single_include + vk_build_default vk_color_self_barrier vk_command_buffer_batching vk_draw_area_batch_boundary diff --git a/runtime/include/debug_trace_ranges.h b/runtime/include/debug_trace_ranges.h new file mode 100644 index 00000000..5175333b --- /dev/null +++ b/runtime/include/debug_trace_ranges.h @@ -0,0 +1,30 @@ +#ifndef PSXRECOMP_DEBUG_TRACE_RANGES_H +#define PSXRECOMP_DEBUG_TRACE_RANGES_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct PSXDebugTraceRange { + uint32_t lo; + uint32_t hi; +} PSXDebugTraceRange; + +/* + * Parse "lo,hi[;lo,hi...]" into masked-physical, half-open ranges. + * + * Returns the number of ranges, or -1 for malformed/reversed input and -2 + * when the destination is too small. Callers must discard the output on any + * negative result. + */ +int psx_debug_parse_trace_ranges(const char *spec, + PSXDebugTraceRange *ranges, + int capacity); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/psx_sdl.h b/runtime/include/psx_sdl.h index ef1ceb70..1749b46f 100644 --- a/runtime/include/psx_sdl.h +++ b/runtime/include/psx_sdl.h @@ -11,7 +11,6 @@ #define SDL_ENABLE_OLD_NAMES #define SDL_FUNCTION_POINTER_IS_VOID_POINTER #include -#include #ifndef SDL_WINDOW_SHOWN #define SDL_WINDOW_SHOWN 0 diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 5a267016..673854aa 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -197,6 +197,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/sio.c ${PSXRECOMP_ROOT}/runtime/src/memcard.c ${PSXRECOMP_ROOT}/runtime/src/debug_server.c + ${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c ${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c ${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c ${PSXRECOMP_ROOT}/runtime/src/fntrace.c diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 706c05fe..b63d7a84 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -41,6 +41,7 @@ #include "crash_trace.h" #include "gpu_gl_renderer.h" #include "lockstep.h" +#include "debug_trace_ranges.h" #include #include @@ -385,8 +386,8 @@ static uint32_t s_wtrace_head = 0; static WriteTraceEntry *s_wtrace_boot = NULL; static uint64_t s_wtrace_boot_total = 0; /* matching writes ever seen */ static uint32_t s_wtrace_boot_count = 0; /* entries retained */ -#define WTRACE_BOOT_MAX_RANGES 12 -static struct { uint32_t lo, hi; } s_wtrace_boot_ranges[WTRACE_BOOT_MAX_RANGES]; +#define WTRACE_BOOT_MAX_RANGES 32 +static PSXDebugTraceRange s_wtrace_boot_ranges[WTRACE_BOOT_MAX_RANGES]; static int s_wtrace_boot_range_count = 0; /* Multi-range filter: up to 64 [lo, hi) address ranges. Boot defaults @@ -13018,6 +13019,40 @@ void debug_server_init(int port) s_wtrace_trans_range_count = 10; #endif + /* + * Generic extraction of NyperYuhgard's Crash Bash write watcher: arm + * caller-selected ranges before guest execution and retain their first + * writes in the existing register-rich boot trace. Unlike the original + * hard-coded stderr trap, this works for any title and is queryable over + * TCP after the event. + */ + { + const char *spec = getenv("PSX_WTRACE_BOOT"); + PSXDebugTraceRange parsed[16]; + int count = (spec && *spec) + ? psx_debug_parse_trace_ranges( + spec, parsed, (int)(sizeof(parsed) / sizeof(parsed[0]))) + : 0; + + if (count < 0) { + fprintf(stderr, + "psxrecomp: ignoring invalid PSX_WTRACE_BOOT='%s' " + "(expected lo,hi[;lo,hi...])\n", + spec ? spec : ""); + } else if (count > WTRACE_BOOT_MAX_RANGES - + s_wtrace_boot_range_count) { + fprintf(stderr, + "psxrecomp: ignoring PSX_WTRACE_BOOT: too many ranges " + "(%d configured, %d available)\n", + count, WTRACE_BOOT_MAX_RANGES - + s_wtrace_boot_range_count); + } else { + for (int i = 0; i < count; ++i) { + s_wtrace_boot_ranges[s_wtrace_boot_range_count++] = parsed[i]; + } + } + } + /* Tier 1: heap-allocate MMIO trace ring buffer (2 MB). */ if (!s_mmio_trace) { s_mmio_trace = (MmioTraceEntry *)calloc(MMIO_TRACE_CAP, sizeof(MmioTraceEntry)); diff --git a/runtime/src/debug_trace_ranges.c b/runtime/src/debug_trace_ranges.c new file mode 100644 index 00000000..83cc2fda --- /dev/null +++ b/runtime/src/debug_trace_ranges.c @@ -0,0 +1,93 @@ +#include "debug_trace_ranges.h" + +#include +#include +#include +#include + +static const char *skip_space(const char *p) +{ + while (*p && isspace((unsigned char)*p)) { + ++p; + } + return p; +} + +static int parse_u32(const char **cursor, uint32_t *value) +{ + char *end = NULL; + unsigned long parsed; + const char *start = skip_space(*cursor); + + if (!*start || *start == '-') { + return 0; + } + + errno = 0; + parsed = strtoul(start, &end, 0); + if (end == start || errno == ERANGE || parsed > UINT32_MAX) { + return 0; + } + + *cursor = end; + *value = (uint32_t)parsed; + return 1; +} + +int psx_debug_parse_trace_ranges(const char *spec, + PSXDebugTraceRange *ranges, + int capacity) +{ + const char *cursor; + int count = 0; + + if (!spec || !ranges || capacity < 0) { + return -1; + } + + cursor = skip_space(spec); + if (!*cursor) { + return 0; + } + + for (;;) { + uint32_t lo; + uint32_t hi; + + if (!parse_u32(&cursor, &lo)) { + return -1; + } + cursor = skip_space(cursor); + if (*cursor++ != ',') { + return -1; + } + if (!parse_u32(&cursor, &hi)) { + return -1; + } + + lo &= 0x1FFFFFFFu; + hi &= 0x1FFFFFFFu; + if (hi <= lo) { + return -1; + } + if (count >= capacity) { + return -2; + } + + ranges[count].lo = lo; + ranges[count].hi = hi; + ++count; + + cursor = skip_space(cursor); + if (!*cursor) { + return count; + } + if (*cursor++ != ';') { + return -1; + } + cursor = skip_space(cursor); + if (!*cursor) { + return -1; + } + } +} diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 181cddcc..23e39501 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -72,6 +72,14 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */ #endif #include "psx_sdl.h" +#if defined(PSX_SDL3) +/* + * SDL_main.h is a single-header implementation in SDL3. Keep it in the one + * translation unit that defines main(); including it through psx_sdl.h makes + * every SDL-using source emit WinMain under MinGW. + */ +#include +#endif #include "psx_sdl_audio.h" #if defined(PSX_WEB) #include diff --git a/runtime/tests/test_debug_trace_ranges.c b/runtime/tests/test_debug_trace_ranges.c new file mode 100644 index 00000000..267df50b --- /dev/null +++ b/runtime/tests/test_debug_trace_ranges.c @@ -0,0 +1,41 @@ +#include "debug_trace_ranges.h" + +#include +#include + +int main(void) +{ + PSXDebugTraceRange ranges[3] = {{0}}; + int count; + + assert(psx_debug_parse_trace_ranges(NULL, ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("", ranges, 3) == 0); + assert(psx_debug_parse_trace_ranges(" ", ranges, 3) == 0); + + count = psx_debug_parse_trace_ranges( + "0x000B3A80,0x000B3B00", ranges, 3); + assert(count == 1); + assert(ranges[0].lo == 0x000B3A80u); + assert(ranges[0].hi == 0x000B3B00u); + + count = psx_debug_parse_trace_ranges( + " 0x800B3A80 , 0x800B3B00 ; 4096, 4352 ", ranges, 3); + assert(count == 2); + assert(ranges[0].lo == 0x000B3A80u); + assert(ranges[0].hi == 0x000B3B00u); + assert(ranges[1].lo == 4096u); + assert(ranges[1].hi == 4352u); + + assert(psx_debug_parse_trace_ranges("1,1", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("2,1", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("-1,2", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("1", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("1,2;", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("1,2x", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges( + "0x100000000,0x100000001", ranges, 3) == -1); + assert(psx_debug_parse_trace_ranges("1,2;3,4", ranges, 1) == -2); + + puts("debug trace range parser: ok"); + return 0; +} diff --git a/runtime/tests/test_sdl3_main_single_include.py b/runtime/tests/test_sdl3_main_single_include.py new file mode 100644 index 00000000..71a4ad2b --- /dev/null +++ b/runtime/tests/test_sdl3_main_single_include.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Keep SDL3's single-header entry-point implementation in main.cpp only.""" + +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[2] +SDL_MAIN_HEADER = "" + + +def main() -> int: + runtime = ROOT / "runtime" + owners = [] + for pattern in ("*.c", "*.cpp", "*.h"): + for path in runtime.rglob(pattern): + source = path.read_text(encoding="utf-8") + if SDL_MAIN_HEADER in source: + owners.append(path.relative_to(ROOT).as_posix()) + + if owners != ["runtime/src/main.cpp"]: + raise AssertionError( + "SDL_main.h must be included exactly once by runtime/src/main.cpp; " + f"found {owners}" + ) + + main_cpp = (runtime / "src/main.cpp").read_text(encoding="utf-8") + guard = ( + "#if defined(PSX_SDL3)\n" + "/*\n" + " * SDL_main.h is a single-header implementation" + ) + if guard not in main_cpp: + raise AssertionError("SDL_main.h include is no longer guarded by PSX_SDL3") + + print("PASS: SDL3 entry-point implementation has one owner") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except AssertionError as exc: + print(f"FAIL: {exc}") + sys.exit(1)