Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 24 additions & 6 deletions TCP_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):

Expand Down
2 changes: 1 addition & 1 deletion docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
28 changes: 28 additions & 0 deletions docs/internal/upstream/pr13-generic-boot-write-watch.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 13 additions & 4 deletions recompiler/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions runtime/include/debug_trace_ranges.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef PSXRECOMP_DEBUG_TRACE_RANGES_H
#define PSXRECOMP_DEBUG_TRACE_RANGES_H

#include <stdint.h>

#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
1 change: 0 additions & 1 deletion runtime/include/psx_sdl.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#define SDL_ENABLE_OLD_NAMES
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#ifndef SDL_WINDOW_SHOWN
#define SDL_WINDOW_SHOWN 0
Expand Down
1 change: 1 addition & 0 deletions runtime/runtime.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions runtime/src/debug_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "crash_trace.h"
#include "gpu_gl_renderer.h"
#include "lockstep.h"
#include "debug_trace_ranges.h"

#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
93 changes: 93 additions & 0 deletions runtime/src/debug_trace_ranges.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#include "debug_trace_ranges.h"

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>

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;
}
}
}
8 changes: 8 additions & 0 deletions runtime/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SDL3/SDL_main.h>
#endif
#include "psx_sdl_audio.h"
#if defined(PSX_WEB)
#include <emscripten/emscripten.h>
Expand Down
41 changes: 41 additions & 0 deletions runtime/tests/test_debug_trace_ranges.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include "debug_trace_ranges.h"

#include <assert.h>
#include <stdio.h>

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;
}
Loading