From 52eb8e8a24ca3715afc835b1994f69e269bf4796 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 21:38:36 -0700 Subject: [PATCH 1/8] Fix encoded CD-ROM interrupt mask matching Adapt the CD interrupt mask/status comparison from kem0x PR #14 and centralize it in a tested helper. The HC05 exposes numeric INT1..INT5 reasons but gates the line with a direct mask/status AND, so the common 0x07 mask must also pass INT4 and INT5. Source: https://github.com/mstan/psxrecomp/pull/14 Source-commit: https://github.com/mstan/psxrecomp/commit/dd9e5c65073dd41fbac2d965d2f945a866ec59e2 Co-authored-by: Kareem Olim --- .../kem0x-pr14-cdrom-encoded-interrupts.md | 29 +++++++++++ runtime/src/cdrom.c | 8 +-- runtime/src/cdrom_irq.h | 15 ++++++ runtime/tests/test_cdrom_irq_mask.c | 50 +++++++++++++++++++ 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md create mode 100644 runtime/src/cdrom_irq.h create mode 100644 runtime/tests/test_cdrom_irq_mask.c diff --git a/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md b/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md new file mode 100644 index 00000000..ef6a92b5 --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md @@ -0,0 +1,29 @@ +# kem0x PR 14: encoded CD-ROM interrupt matching + +Evaluated and adapted on 2026-07-13. + +## Provenance + +The CD-ROM interrupt mask fix was rebuilt from Kareem Olim's +[PSXrecomp PR 14](https://github.com/mstan/psxrecomp/pull/14), specifically +commit +[`dd9e5c65073dd41fbac2d965d2f945a866ec59e2`](https://github.com/mstan/psxrecomp/commit/dd9e5c65073dd41fbac2d965d2f945a866ec59e2) +(`Deliver encoded CD interrupt reasons correctly`). The implementation here +centralizes the mask/status comparison in a tested internal helper rather than +copying the source patch verbatim. + +The behavior follows the +[PSX-SPX CD-ROM register documentation](https://psx-spx.consoledev.net/cdromdrive/#0x1f801803-read-banks-1-and-3-hintsts): +HINTSTS bits 0..2 are an encoded HC05 interrupt reason, but the interrupt line +is asserted whenever `HINTMSK & HINTSTS` is nonzero. Converting the reason to a +one-hot bit before applying the mask is therefore incorrect. A common `0x07` +mask must deliver encoded INT4 (DataEnd) and INT5 (DiskError) as well as +INT1--INT3. + +## Deliberately excluded + +This branch does not import PR 14's WebAssembly/web frontend, Pepsiman-specific +hooks or patches, CD-DA playback changes, SPU changes, raster/rendering changes, +or any other runtime and build-system material. Those changes have different +scope and review requirements and are not needed for encoded interrupt mask +matching. diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 79682f7b..b8ce75bd 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -11,6 +11,7 @@ */ #include "cdrom.h" +#include "cdrom_irq.h" #include "dma.h" #include "spu.h" #include "event_ring.h" @@ -578,7 +579,8 @@ static void present_cdrom_irq(void) { * the guest's command/ack write — and thus from being lost to that ISR's * own trailing INTC ack. */ if (cdrom_irq_present_delay > 0) return; - if (irq_flag && (irq_enable & (1 << (irq_flag - 1))) && !cdrom_intc_request_latched) { + if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag) && + !cdrom_intc_request_latched) { psx_irq_raise(2, irq_flag); /* IRQ_CDROM; detail = CD response/IRQ type */ cdrom_intc_request_latched = 1; cdrom_intc_latched_generation = cdrom_irq_generation; @@ -590,7 +592,7 @@ static void present_cdrom_irq(void) { /* Fire CDROM IRQ into the interrupt controller (explicit, per command/response). * Trace the masked case here only (low-frequency); refresh stays silent. */ static void fire_cdrom_irq(void) { - if (irq_flag && !(irq_enable & (1 << (irq_flag - 1)))) { + if (irq_flag && !cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) { trace_cdrom('f', 0, irq_flag, 0); } present_cdrom_irq(); @@ -1841,7 +1843,7 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) { if (!(i_mask & (1u << 2))) return 0xFFFFFFFFu; /* IRQ_CDROM masked */ uint32_t best = 0xFFFFFFFFu; /* Armed response awaiting presentation (will raise bit2 when delay hits 0). */ - if (irq_flag && (irq_enable & (1u << (irq_flag - 1)))) { + if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) { uint32_t d = cdrom_irq_present_delay > 0 ? (uint32_t)cdrom_irq_present_delay : 0u; if (d < best) best = d; } diff --git a/runtime/src/cdrom_irq.h b/runtime/src/cdrom_irq.h new file mode 100644 index 00000000..82e03e76 --- /dev/null +++ b/runtime/src/cdrom_irq.h @@ -0,0 +1,15 @@ +#ifndef PSXRECOMP_CDROM_IRQ_H +#define PSXRECOMP_CDROM_IRQ_H + +#include + +/* HINTSTS bits 0..2 contain an encoded interrupt reason (INT1..INT5), not + * independent one-hot flags. The controller nevertheless gates its IRQ line + * by directly ANDing HINTSTS with HINTMSK. In particular, mask 0x07 enables + * all five encoded reasons, including INT4 and INT5. */ +static inline int cdrom_irq_mask_matches_reason(uint8_t irq_enable, + uint8_t irq_reason) { + return (irq_enable & irq_reason & 0x07u) != 0; +} + +#endif /* PSXRECOMP_CDROM_IRQ_H */ diff --git a/runtime/tests/test_cdrom_irq_mask.c b/runtime/tests/test_cdrom_irq_mask.c new file mode 100644 index 00000000..645702eb --- /dev/null +++ b/runtime/tests/test_cdrom_irq_mask.c @@ -0,0 +1,50 @@ +/* + * Validate PS1 CD-ROM interrupt mask matching. + * + * Build: + * cc -std=c99 -Wall -Wextra -Werror -I../src \ + * -o test_cdrom_irq_mask test_cdrom_irq_mask.c + */ +#include "cdrom_irq.h" + +#include +#include + +static int failures; + +#define CHECK(expected, mask, reason, label) do { \ + int actual = cdrom_irq_mask_matches_reason((uint8_t)(mask), \ + (uint8_t)(reason)); \ + if (actual != (expected)) { \ + fprintf(stderr, "FAIL: %s (mask=0x%02X reason=%u expected=%d got=%d)\n",\ + (label), (unsigned)(mask), (unsigned)(reason), \ + (expected), actual); \ + failures++; \ + } \ +} while (0) + +int main(void) { + /* The conventional 0x07 mask must deliver every encoded HC05 reason. */ + for (uint8_t reason = 1; reason <= 5; reason++) { + CHECK(1, 0x07, reason, "mask 0x07 enables INT1 through INT5"); + } + + /* The mask/status comparison is bitwise, not a reason-to-one-hot lookup. */ + CHECK(0, 0x01, 2, "bit 0 does not match INT2"); + CHECK(1, 0x01, 3, "bit 0 matches encoded INT3"); + CHECK(0, 0x02, 4, "bit 1 does not match INT4"); + CHECK(1, 0x04, 4, "bit 2 matches encoded INT4"); + CHECK(1, 0x01, 5, "bit 0 matches encoded INT5"); + CHECK(1, 0x04, 5, "bit 2 matches encoded INT5"); + + CHECK(0, 0x00, 1, "zero mask suppresses an interrupt"); + CHECK(0, 0x07, 0, "reason zero means no interrupt"); + CHECK(0, 0x18, 5, "audio-buffer mask bits do not match an HC05 reason"); + + if (failures) { + fprintf(stderr, "FAILED (%d)\n", failures); + return 1; + } + puts("ALL PASS"); + return 0; +} From 76ddcc008a72b1dbb8d9bee227e02ae6549caa99 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 21:40:11 -0700 Subject: [PATCH 2/8] test(spu): pin END-only voice mute semantics Document the overlap with kem0x PR #14 and explicitly record the game-specific and unrelated changes excluded from this branch. Co-authored-by: Kareem Olim --- .../upstream/kem0x-pr14-spu-end-mute.md | 38 +++++ runtime/tests/test_spu_end_without_repeat.c | 131 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 docs/internal/upstream/kem0x-pr14-spu-end-mute.md create mode 100644 runtime/tests/test_spu_end_without_repeat.c diff --git a/docs/internal/upstream/kem0x-pr14-spu-end-mute.md b/docs/internal/upstream/kem0x-pr14-spu-end-mute.md new file mode 100644 index 00000000..f93111ff --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-spu-end-mute.md @@ -0,0 +1,38 @@ +# kem0x PR #14: SPU END-without-REPEAT behavior + +Evaluated on 2026-07-13 against psxrecomp master `dde268dc0fb9daf8fe6529f4aebfe80995350334`. + +## Source and credit + +- Pull request: [mstan/psxrecomp#14](https://github.com/mstan/psxrecomp/pull/14) +- Evaluated commit: [`b2597df6d5905a0e183b5309a9b84720cd417e19`](https://github.com/mstan/psxrecomp/commit/b2597df6d5905a0e183b5309a9b84720cd417e19) +- Original author: Kareem Olim (`Kareem Olim `) + +The generic behavior identified in that commit is that an ADPCM block carrying +END without REPEAT must redirect decoding to the voice's repeat address, enter +Release, and immediately clear the envelope. This prevents a one-shot voice +from leaking or looping unrelated sample RAM after its terminator. + +Current master already implements the same behavior independently in commit +`462e2bf5fdb4d46b935322bf7f782fd424aea584`. Therefore this branch does not +replace that runtime code. It adds a focused regression test that pins the +shared behavior: END-only becomes silent and inactive at the boundary, resumes +decoding at the repeat address, and emits one stop event; END+REPEAT remains +audible and active. + +## Explicit exclusions + +The following material from the source commit was reviewed and intentionally +not imported: + +- `PEPSIMAN_SFX_RETRIGGER_GUARD` and its address-based burst/cooldown policy: + game-specific compensation for Pepsiman timing, not generic SPU behavior. +- The `PSX_WEB` reduction of `SPU_EVENT_CAP`: WebAssembly memory sizing is + unrelated to ADPCM END semantics and belongs in a separate change. +- `SpuVoice.start_addr`: introduced for the excluded retrigger policy and not + needed for the generic END transition. +- All other PR #14 changes: CD-ROM interrupts, CD-DA, raster timing, build + changes, and game-specific hooks require independent review and branches. + +No Pepsiman compile-time hook or game-specific playback heuristic is present in +this branch. diff --git a/runtime/tests/test_spu_end_without_repeat.c b/runtime/tests/test_spu_end_without_repeat.c new file mode 100644 index 00000000..e883276b --- /dev/null +++ b/runtime/tests/test_spu_end_without_repeat.c @@ -0,0 +1,131 @@ +/* + * Regression test for an ADPCM END block without REPEAT. + * + * The test includes spu.c so it can stage a voice exactly at the block + * boundary, without exposing emulator internals in the runtime API. + * + * Build from runtime/tests: + * gcc -std=c99 -I../include -o test_spu_end_without_repeat.exe \ + * test_spu_end_without_repeat.c + */ + +#include +#include +#include + +#include "../src/spu.c" + +/* Dependencies owned by the surrounding runtime are inert in this unit test. */ +uint64_t s_frame_count; + +uint64_t psx_get_cycle_count(void) { return 0; } + +void audio_trace_pcm(int tap, const int16_t *stereo, int frames) { + (void)tap; + (void)stereo; + (void)frames; +} + +void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) { + (void)kind; + (void)a; + (void)b; +} + +bool spu_shadow_enabled(void) { return false; } +void spu_shadow_reset(void) {} +void spu_shadow_process(int16_t *canon, int frames) { + (void)canon; + (void)frames; +} + +static int failures; + +#define CHECK(condition, message) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL: %s\n", message); \ + failures++; \ + } else { \ + printf("ok: %s\n", message); \ + } \ + } while (0) + +static void fill_block(uint32_t addr, uint8_t flags, uint8_t packed) { + spu_ram[addr] = 0x00; /* filter 0, shift 0 */ + spu_ram[addr + 1u] = flags; + memset(spu_ram + addr + 2u, packed, 14u); +} + +static SpuVoice *stage_end_boundary(uint8_t flags, uint32_t repeat_addr) { + SpuVoice *v = &voices[0]; + memset(v, 0, sizeof(*v)); + v->active = 1; + v->cur_addr = 0x310u; /* the block after the terminator */ + v->repeat_addr = repeat_addr; + v->sample_idx = SPU_BLOCK_SAMPLES; + v->env_level = 0x6000u; + v->adsr_phase = ADSR_SUSTAIN; + v->flags = flags; + spu_regs[2] = 0x1000u; /* one decoded sample per output sample */ + return v; +} + +static void test_end_without_repeat_is_silent(void) { + const uint32_t repeat_addr = 0x120u; + spu_init(); + + /* Distinguish the required repeat-address decode from an incorrect + * forward decode. Both blocks are deliberately non-silent: the envelope, + * rather than convenient zero sample data, must make the result silent. */ + fill_block(repeat_addr, 0x04u, 0x11u); + fill_block(0x310u, 0x00u, 0x77u); + SpuVoice *v = stage_end_boundary(0x01u, repeat_addr); + + int16_t sample = voice_next_sample(0); + + CHECK(sample == 0, "END without REPEAT emits silence at the boundary"); + CHECK(v->env_level == 0, "END without REPEAT forces the envelope to zero"); + CHECK(v->adsr_phase == ADSR_RELEASE, + "END without REPEAT enters Release"); + CHECK(!v->active, "zero-level Release deactivates the voice"); + CHECK(v->samples[0] == 4096, + "decoder resumes at the latched repeat address, not forward RAM"); + CHECK(spu_event_total() == 1, "END stop records exactly one event"); + + SpuEvent event; + CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_STOP, + "END stop event has the stop kind"); + CHECK(event.addr == repeat_addr, + "END stop event reports the redirected repeat address"); + + (void)voice_next_sample(0); + CHECK(spu_event_total() == 1, + "an inactive one-shot does not retrigger its END transition"); +} + +static void test_end_with_repeat_keeps_playing(void) { + const uint32_t repeat_addr = 0x180u; + spu_init(); + fill_block(repeat_addr, 0x00u, 0x11u); + SpuVoice *v = stage_end_boundary(0x03u, repeat_addr); + + int16_t sample = voice_next_sample(0); + + CHECK(sample != 0, "END with REPEAT remains audible"); + CHECK(v->env_level != 0, "END with REPEAT preserves the envelope"); + CHECK(v->adsr_phase == ADSR_SUSTAIN, + "END with REPEAT preserves the ADSR phase"); + CHECK(v->active, "END with REPEAT keeps the voice active"); + + SpuEvent event; + CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_LOOP, + "END with REPEAT records a loop event"); +} + +int main(void) { + test_end_without_repeat_is_silent(); + test_end_with_repeat_keeps_playing(); + printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures); + return failures ? 1 : 0; +} From 05d4f35bd6314d56ac95e6cec4b90655950dbd86 Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:53:38 +0200 Subject: [PATCH 3/8] build: MSVC portability for runtime and generated code - psx_cyc.h / psx_cycles.c: _BitScanForward/_BitScanReverse shims for __builtin_ctz/__builtin_clz under _MSC_VER - full_function_emitter.cpp / main_psx.cpp: emit a portable .CRT$XCU init-pointer instead of GCC-only __attribute__((constructor)) in generated dispatch/game code - main.cpp: declare cross-language globals extern "C" so MSVC links them against their C definitions (no-op for GCC/Clang) Co-Authored-By: Claude Fable 5 --- .../upstream/msvc-portability-shaneomac.md | 41 ++++++++ recompiler/src/full_function_emitter.cpp | 10 +- recompiler/src/main_psx.cpp | 10 +- recompiler/tests/test_msvc_cps_codegen.py | 98 +++++++++++++++++++ runtime/include/psx_cyc.h | 9 ++ runtime/src/main.cpp | 15 +++ runtime/src/psx_cycles.c | 9 ++ 7 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 docs/internal/upstream/msvc-portability-shaneomac.md create mode 100644 recompiler/tests/test_msvc_cps_codegen.py diff --git a/docs/internal/upstream/msvc-portability-shaneomac.md b/docs/internal/upstream/msvc-portability-shaneomac.md new file mode 100644 index 00000000..5da3eec8 --- /dev/null +++ b/docs/internal/upstream/msvc-portability-shaneomac.md @@ -0,0 +1,41 @@ +# MSVC portability from Shaneomac PR #16 + +## Source and credit + +- Source PR: [mstan/psxrecomp#16](https://github.com/mstan/psxrecomp/pull/16) +- Source commit: [`df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb`](https://github.com/mstan/psxrecomp/commit/df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb) +- Author preserved by cherry-pick: Martin Penkava + ``. +- The source commit's Claude Fable 5 co-author trailer is also preserved. + +## Adapted scope + +The source commit was applied as a focused portability change: + +- MSVC bit-scan intrinsics replace unavailable GCC/Clang builtins; +- generated CPS startup markers use an MSVC `.CRT$XCU` initializer and retain + the constructor path for GCC/Clang; and +- runtime globals defined by C translation units receive explicit C linkage in + the C++ entry point. + +`recompiler/tests/test_msvc_cps_codegen.py` generates a minimal game dispatch +and checks that both compiler startup paths are emitted. + +## Validation in this branch + +- Visual Studio 2022/MSVC 19.41 built `psxrecomp-game`, `psxrecomp-bios`, and + `l2_structural_test`; the structural suite passed 44/44. +- The MSVC-built game recompiler passed + `test_msvc_cps_codegen.py` on generated output. +- A freshly generated BIOS runtime compiled all 63 C/C++ object steps under + MSVC, including the new intrinsic and C-linkage paths. The final BIOS-only + link remains blocked by the baseline's unrelated missing + `psx_game_address_in_text` and `psx_game_text_native_ok` definitions. No + change for that pre-existing game-symbol contract is included here. + +## Explicitly excluded + +No other PR #16 material was imported. In particular, this branch excludes the +SmackDown-specific batch files, dynamic-code discovery fallback, SPU changes, +primitive rejection, Vulkan diagnostics/optimization, and CRT shaders. Those +items require independent review and provenance records. diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index aed4c4fd..ed10a713 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -1656,9 +1656,17 @@ void FullFunctionEmitter::emit_dispatch( // RECURSION_BUG.md §25 — mark CPS mode at startup for runtime code that // must emit the CPS contract (the overlay sljit JIT, overlay_sljit.c). out += "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n"; - out += "__attribute__((constructor)) static void psx_cps_mark_bios(void) {\n"; + out += "static void psx_cps_mark_bios(void) {\n"; out += " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n"; out += "}\n"; + // Run psx_cps_mark_bios before main(). __attribute__((constructor)) is + // GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU. + out += "#if defined(_MSC_VER)\n"; + out += "#pragma section(\".CRT$XCU\", read)\n"; + out += "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_bios_ctor)(void) = psx_cps_mark_bios;\n"; + out += "#else\n"; + out += "__attribute__((constructor)) static void psx_cps_mark_bios_ctor(void) { psx_cps_mark_bios(); }\n"; + out += "#endif\n"; } } diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index c7f5b811..f60d1fe7 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -1086,9 +1086,17 @@ int main(int argc, char** argv) { // JIT (overlay_sljit.c) emits the CPS contract. Static ctor: no clash // with the BIOS dispatch's marker. ds << "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n"; - ds << "__attribute__((constructor)) static void psx_cps_mark_game(void) {\n"; + ds << "static void psx_cps_mark_game(void) {\n"; ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n"; ds << "}\n"; + // Run psx_cps_mark_game before main(). __attribute__((constructor)) is + // GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU. + ds << "#if defined(_MSC_VER)\n"; + ds << "#pragma section(\".CRT$XCU\", read)\n"; + ds << "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;\n"; + ds << "#else\n"; + ds << "__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) { psx_cps_mark_game(); }\n"; + ds << "#endif\n"; } std::ofstream dispatch_file(dispatch_filename); diff --git a/recompiler/tests/test_msvc_cps_codegen.py b/recompiler/tests/test_msvc_cps_codegen.py new file mode 100644 index 00000000..02e21181 --- /dev/null +++ b/recompiler/tests/test_msvc_cps_codegen.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Verify that CPS game output carries both MSVC and GCC/Clang startup hooks. + +Usage: python test_msvc_cps_codegen.py [--recompiler ] +""" + +import argparse +import os +import struct +import subprocess +import sys +import tempfile + + +LOAD = 0x80010000 + + +def make_exe(): + header = bytearray(2048) + header[0:8] = b"PS-X EXE" + struct.pack_into(" +#if defined(_MSC_VER) +#include /* MSVC intrinsics: _BitScanForward (no __builtin_ctz) */ +#endif #include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */ #ifdef __cplusplus @@ -49,7 +52,13 @@ static inline void psx_cyc_base(CPUState* cpu) { static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) { reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */ while (reg_mask) { +#if defined(_MSC_VER) + unsigned long _psx_ctz_idx; + _BitScanForward(&_psx_ctz_idx, reg_mask); + unsigned n = (unsigned)_psx_ctz_idx; +#else unsigned n = (unsigned)__builtin_ctz(reg_mask); +#endif cpu->read_absorb[n] = 0u; reg_mask &= reg_mask - 1u; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 9668c6ec..f5d5717f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -93,6 +93,21 @@ extern "C" uint64_t gte_get_exec_count(void); +/* Cross-language globals defined in C translation units. Declared extern "C" at + * file scope so MSVC gives them C linkage (matching the C definitions); without + * this MSVC name-mangles the C++ references and they fail to link. GCC/Clang do + * not mangle namespace-scope variables, so this is a no-op there. The existing + * block-scope `extern` redeclarations inside functions inherit this C linkage. */ +extern "C" { + extern uint64_t psx_cycle_count; + extern uint64_t s_frame_count; + extern uint32_t g_overlay_region_floor; + extern int g_psx_cps_mode; + extern uint64_t g_slice_fired, g_slice_irq_taken, g_dirty_ram_insns_run; + extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter; + extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want; +} + /* memory.c */ extern "C" void memory_init(const char* bios_path); extern "C" void memory_set_sr_ptr(const uint32_t *p); diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 1d92a7e5..1610d522 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -3,6 +3,9 @@ #include "psx_cycles.h" #include "cpu_state.h" #include +#if defined(_MSC_VER) +#include /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */ +#endif #include "cdrom.h" #include "dma.h" #include "interrupts.h" @@ -451,7 +454,13 @@ static const uint8_t PSX_MULT_TAB24[24] = { static inline uint32_t psx_clz32(uint32_t v) { /* v is never 0 here (callers OR in 0x400). */ +#if defined(_MSC_VER) + unsigned long _idx; + _BitScanReverse(&_idx, v); /* index of highest set bit */ + return (uint32_t)(31u - _idx); +#else return (uint32_t)__builtin_clz(v); +#endif } uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */ From 5b8098a9d997424cbd57829b8ae11369ab06935a Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 21:41:31 -0700 Subject: [PATCH 4/8] runtime: guard overlay loader before initialization Adapt only the fail-closed overlay lifecycle checks from mstan/psxrecomp#16 commit e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba. Add a focused structural regression test and record the excluded dirty-interpreter and game/runtime work. Co-authored-by: Martin Penkava Co-authored-by: Claude Fable 5 --- .../shaneomac-pr16-overlay-init-guard.md | 33 +++++++ runtime/src/overlay_loader.c | 5 +- runtime/tests/test_overlay_init_guard.py | 85 +++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 docs/internal/upstream/shaneomac-pr16-overlay-init-guard.md create mode 100644 runtime/tests/test_overlay_init_guard.py diff --git a/docs/internal/upstream/shaneomac-pr16-overlay-init-guard.md b/docs/internal/upstream/shaneomac-pr16-overlay-init-guard.md new file mode 100644 index 00000000..685f9ed9 --- /dev/null +++ b/docs/internal/upstream/shaneomac-pr16-overlay-init-guard.md @@ -0,0 +1,33 @@ +# Shaneomac PR #16: overlay-loader initialization guard + +| Field | Value | +|---|---| +| Upstream PR | | +| Source commit | [`e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba`](https://github.com/mstan/psxrecomp/commit/e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba) | +| Source author | Martin Penkava (`shaneomac1337`) `` | +| Source co-author | Claude Fable 5 `` | +| Port base | `dde268dc0fb9daf8fe6529f4aebfe80995350334` | +| Evaluated/ported | 2026-07-13 (America/Los_Angeles) | + +## Included + +Only the overlay-loader lifecycle guards from the source commit were adapted. +`overlay_loader_dispatch()`, `overlay_loader_is_candidate()`, and +`overlay_loader_call_native()` now return without consulting overlay indexes +until `overlay_loader_init()` finishes constructing them and sets `s_active`. + +The focused regression test is +`runtime/tests/test_overlay_init_guard.py`. + +## Explicitly excluded + +- The source commit's decodable-word/high-RAM dirty-interpreter fallback in + `runtime/src/dirty_ram_interp.c`. +- All SmackDown-specific behavior and data. +- Every other PR #16 change, including Vulkan, audio/SPU, GPU primitive-size, + MSVC portability, and helper-script work. + +The source commit combined the lifecycle guard with a separate discovery +fallback. This port intentionally does not cherry-pick that commit; it adapts +only the three fail-closed `s_active` checks so the framework change remains +game-agnostic and reviewable in isolation. diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index b0efd562..2c8f5687 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2390,6 +2390,7 @@ static int overlay_find_by_range(uint32_t phys) { int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; + if (!s_active) return 0; int lazy_loaded = 0; retry_candidates: int head = idx_head(phys); @@ -2883,6 +2884,7 @@ static FpEnt s_fp[FP_CAP]; static uint64_t s_fp_seq = 0; int overlay_loader_is_candidate(uint32_t phys) { + if (!s_active) return 0; phys &= 0x1FFFFFFFu; return idx_head(phys) >= 0 || lazy_has_exact_entry(phys); } @@ -3229,7 +3231,8 @@ void overlay_fp_log(uint32_t addr, const uint32_t *in_regs, * leaks (root cause of the dwarf->overworld native blue screen). * Returns 1 iff a native candidate ran. */ int overlay_loader_call_native(CPUState *cpu, uint32_t addr) { - if (!s_native_exec) return 0; /* interp mode: keep the legacy inline path */ + if (!s_active || !s_native_exec) + return 0; /* inactive/interp mode: keep the legacy inline path */ uint32_t phys = addr & 0x1FFFFFFFu; if (idx_head(phys) < 0 && !lazy_has_exact_entry(phys)) return 0; /* neither a registered nor an exact cached entry */ diff --git a/runtime/tests/test_overlay_init_guard.py b/runtime/tests/test_overlay_init_guard.py new file mode 100644 index 00000000..c14442ef --- /dev/null +++ b/runtime/tests/test_overlay_init_guard.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Structural regression test for pre-init overlay-loader entry points. + +The public dispatch/candidate/native-call entry points can be reached during +early BIOS initialization, before overlay_loader_init() constructs their index +tables. They must fail closed on s_active before consulting those tables. + +Usage: python runtime/tests/test_overlay_init_guard.py +Exit 0 = PASS. +""" + +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "runtime" / "src" / "overlay_loader.c" + + +def function_body(source: str, name: str) -> str: + match = re.search(rf"\b(?:int|void)\s+{re.escape(name)}\s*\([^;]*?\)\s*\{{", source, re.S) + if not match: + raise AssertionError(f"missing function definition: {name}") + + start = match.end() + depth = 1 + for pos in range(start, len(source)): + if source[pos] == "{": + depth += 1 + elif source[pos] == "}": + depth -= 1 + if depth == 0: + return source[start:pos] + raise AssertionError(f"unterminated function definition: {name}") + + +def require_before(body: str, guard_pattern: str, state_pattern: str, name: str) -> None: + guard = re.search(guard_pattern, body) + state = re.search(state_pattern, body) + if not guard: + raise AssertionError(f"{name}: missing pre-init s_active guard") + if not state: + raise AssertionError(f"{name}: test could not find protected index access") + if guard.start() > state.start(): + raise AssertionError(f"{name}: s_active guard occurs after index access") + + +def main() -> int: + source = SOURCE.read_text(encoding="utf-8") + + dispatch = function_body(source, "overlay_loader_dispatch") + require_before( + dispatch, + r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;", + r"idx_head\s*\(", + "overlay_loader_dispatch", + ) + + candidate = function_body(source, "overlay_loader_is_candidate") + require_before( + candidate, + r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;", + r"idx_head\s*\(", + "overlay_loader_is_candidate", + ) + + call_native = function_body(source, "overlay_loader_call_native") + require_before( + call_native, + r"if\s*\(\s*!s_active\s*\|\|\s*!s_native_exec\s*\)\s*return\s+0\s*;", + r"idx_head\s*\(", + "overlay_loader_call_native", + ) + + print("PASS: overlay loader entry points fail closed before initialization") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except AssertionError as exc: + print(f"FAIL: {exc}") + sys.exit(1) From c0fa9b1e89fb6c210011dc25a92b124df43692d4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 21:52:25 -0700 Subject: [PATCH 5/8] overlay: load cached exports on POSIX Port Douglas Vanderveer's manifest-driven macOS/Linux overlay discovery onto the current cache index and exact-dispatch loader. Validate both filename key fields, preserve ABI cleanup and rescan idempotence, and cover the POSIX primitives with a real shared-library fixture. Co-authored-by: douglasjv --- .../upstream/posix-overlay-discovery.md | 37 ++++ runtime/include/overlay_posix.h | 45 +++++ runtime/runtime.cmake | 4 + runtime/src/overlay_loader.c | 177 +++++++++++++++--- runtime/src/overlay_posix.c | 161 ++++++++++++++++ runtime/tests/overlay_posix_fixture.c | 12 ++ runtime/tests/run_overlay_posix_test.sh | 27 +++ runtime/tests/test_overlay_posix.c | 79 ++++++++ 8 files changed, 514 insertions(+), 28 deletions(-) create mode 100644 docs/internal/upstream/posix-overlay-discovery.md create mode 100644 runtime/include/overlay_posix.h create mode 100644 runtime/src/overlay_posix.c create mode 100644 runtime/tests/overlay_posix_fixture.c create mode 100644 runtime/tests/run_overlay_posix_test.sh create mode 100644 runtime/tests/test_overlay_posix.c diff --git a/docs/internal/upstream/posix-overlay-discovery.md b/docs/internal/upstream/posix-overlay-discovery.md new file mode 100644 index 00000000..21bea776 --- /dev/null +++ b/docs/internal/upstream/posix-overlay-discovery.md @@ -0,0 +1,37 @@ +# POSIX overlay discovery provenance + +This change ports one focused capability from Douglas Vanderveer's PSXRecomp +work: Linux/macOS discovery of cached overlay libraries and manifest-driven +resolution of their `func_XXXXXXXX` exports. + +The original implementation was developed while validating the +[R4 recompilation](https://github.com/douglasjv/r4) and appeared in Douglas's +framework commits +[`df7d1faa5f27be0ba357463a763c77efc43e1f91`](https://github.com/douglasjv/psxrecomp-tweaks/commit/df7d1faa5f27be0ba357463a763c77efc43e1f91) +and +[`7abbfdf38b488d3764b96c155549bc930e0521b6`](https://github.com/douglasjv/psxrecomp-tweaks/commit/7abbfdf38b488d3764b96c155549bc930e0521b6). +It is rebuilt here against the newer cache index, ABI tag, codegen namespace, +lazy manifest index, exact native dispatch, and rescan behavior on current +master. The earlier upstream context is +[mstan/psxrecomp PR #15](https://github.com/mstan/psxrecomp/pull/15). + +Included scope: + +- strict `_.dll` validation for both cache-key fields; +- POSIX directory enumeration with GCC-before-TCC deduplication; +- POSIX codegen-tag mismatch diagnostics and ABI preflight cleanup; +- manifest-authoritative `dlsym` registration, with failed/empty libraries + closed and successful libraries retained while their function pointers live; +- additive, idempotent rescan behavior inherited from the current loader. + +Explicitly excluded are frame splitting, SPU changes, forced-interpreter pages, +macOS GL context changes, CPU-copied overlay fallback, game configuration, +R4 addresses or timing policy, and all other R4-specific work. + +The focused Linux test builds a real shared-library fixture, exercises strict +name parsing and directory filtering, detects a sibling codegen tag, resolves a +manifest-style `func_80012345` symbol, and verifies close/reopen behavior: + +```sh +bash runtime/tests/run_overlay_posix_test.sh +``` diff --git a/runtime/include/overlay_posix.h b/runtime/include/overlay_posix.h new file mode 100644 index 00000000..3f3b6473 --- /dev/null +++ b/runtime/include/overlay_posix.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Parsed _.dll cache filename. The parser is shared by the + * Windows and POSIX directory walkers so both platforms reject partial hex + * fields and lookalike suffixes identically. */ +typedef struct PsxOverlayCacheFile { + char name[22]; + char path[768]; + uint32_t region_start; + uint32_t content_crc; + uint64_t mtime; +} PsxOverlayCacheFile; + +typedef int (*PsxOverlayCacheVisitor)(const PsxOverlayCacheFile *file, + void *opaque); + +int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start, + uint32_t *content_crc); + +/* POSIX helpers used by overlay_loader.c and its focused regression test. + * The Windows build provides inert stubs; it keeps using Win32 enumeration and + * LoadLibrary while still sharing the strict filename parser above. */ +int psx_overlay_posix_scan_cache_dir(const char *dir, + PsxOverlayCacheVisitor visitor, + void *opaque); +int psx_overlay_posix_find_other_cache_tag(const char *base_dir, + const char *expected_tag, + char *found_tag, + size_t found_tag_size); +void *psx_overlay_posix_library_open(const char *path, char *error, + size_t error_size); +void *psx_overlay_posix_library_symbol(void *handle, const char *name); +void *psx_overlay_posix_library_entry(void *handle, uint32_t entry); +void psx_overlay_posix_library_close(void *handle); + +#ifdef __cplusplus +} +#endif diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index cc167f95..c4557e7b 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -194,6 +194,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c ${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c ${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c + ${PSXRECOMP_ROOT}/runtime/src/overlay_posix.c ${PSXRECOMP_ROOT}/runtime/src/overlay_compile_worker.c ${PSXRECOMP_ROOT}/runtime/src/overlay_sljit.c ${PSXRECOMP_ROOT}/runtime/src/overlay_backend.c @@ -506,6 +507,9 @@ function(psxrecomp_add_runtime_target target) # by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress. target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32) else() + if(CMAKE_DL_LIBS) + target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS}) + endif() find_package(OpenGL) if(OpenGL_FOUND) target_link_libraries(${target} PRIVATE OpenGL::GL) diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index b0efd562..426dbfa9 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -8,6 +8,7 @@ #include "interrupts.h" #include "debug_server.h" #include "psx_cycles.h" +#include "overlay_posix.h" #include #include #include @@ -18,7 +19,7 @@ # define WIN32_LEAN_AND_MEAN # include #else -# include +# include #endif /* ============================================================================ @@ -1059,6 +1060,26 @@ static int cache_idx_has_basename(const char *fname) { const char *overlay_loader_arch_abi(void) { return PSX_OVERLAY_ARCH_ABI; } +#ifndef _WIN32 +static int add_posix_cache_file(const PsxOverlayCacheFile *file, void *opaque) { + (void)opaque; + if (s_cache_idx_count >= CACHE_IDX_CAP) { + loader_log("*** CACHE INDEX FULL (%d): further DLLs near %s are being " + "IGNORED — their regions will run interpreted. Raise " + "CACHE_IDX_CAP.", CACHE_IDX_CAP, file->path); + return 1; + } + if (cache_idx_has_basename(file->name)) return 0; + CacheEntry *e = &s_cache_idx[s_cache_idx_count++]; + e->region_start = file->region_start; + e->mtime = file->mtime; + e->func_count = 0; + e->indexed_func_count = 0; + snprintf(e->path, sizeof(e->path), "%s", file->path); + return 0; +} +#endif + /* Scan one directory for _.dll cache entries into the index. * `dir` is a full directory path. Idempotent (skips already-indexed paths). */ static void scan_one_cache_dir(const char *dir) { @@ -1069,18 +1090,9 @@ static void scan_one_cache_dir(const char *dir) { HANDLE h = FindFirstFileA(pattern, &fd); if (h == INVALID_HANDLE_VALUE) return; do { - if (strlen(fd.cFileName) != 21) continue; /* 8+1+8+4 = 21 */ - /* Validate the _.dll shape explicitly: region_start 0 is - * LEGAL (the kernel-RAM window starts at phys 0), so a zero parse - * result can't be used as the invalid sentinel. */ - int valid = (fd.cFileName[8] == '_'); - for (int ci = 0; valid && ci < 8; ci++) { - char c = fd.cFileName[ci]; - valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || - (c >= 'a' && c <= 'f'); - } - if (!valid) continue; - uint32_t addr = (uint32_t)strtoul(fd.cFileName, NULL, 16); + uint32_t addr = 0, crc = 0; + if (!psx_overlay_cache_name_parse(fd.cFileName, &addr, &crc)) continue; + (void)crc; if (s_cache_idx_count >= CACHE_IDX_CAP) { /* Never-again (the silent-256 truncation): overflowing the index * means real native coverage is being IGNORED. Shout once. */ @@ -1104,7 +1116,7 @@ static void scan_one_cache_dir(const char *dir) { } while (FindNextFileA(h, &fd)); FindClose(h); #else - (void)dir; + psx_overlay_posix_scan_cache_dir(dir, add_posix_cache_file, NULL); #endif } @@ -1152,10 +1164,82 @@ static void warn_on_cgtag_mismatch(const char *tier) { } while (FindNextFileA(h, &fd)); FindClose(h); #else - (void)tier; + char base[768], expect[64], found[256]; + snprintf(base, sizeof base, "%s/%s/%s/%s", + s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI); + snprintf(expect, sizeof expect, "cg%d_%08x", + PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH); + if (psx_overlay_posix_find_other_cache_tag(base, expect, found, + sizeof(found))) { + loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but " + "shards exist under %s/%s. The autocompile is writing to a " + "DIFFERENT codegen hash than this runtime reads -> ALL overlays " + "run INTERPRETED (slow). Fix overlay_autocompile_cmd's " + "--recompiler/--runtime-include to match THIS build's framework.", + tier, expect, tier, found); + } #endif } +static void cache_idx_remove_path(const char *path) { + for (int i = 0; i < s_cache_idx_count; i++) { + if (strcmp(s_cache_idx[i].path, path) == 0) { + s_cache_idx[i] = s_cache_idx[--s_cache_idx_count]; + return; + } + } +} + +static void remove_posix_dll_and_manifest(const char *dll_path) { +#ifndef _WIN32 + remove(dll_path); + char ranges[800]; + int n = snprintf(ranges, sizeof(ranges), "%s", dll_path); + if (n >= 4 && (size_t)n + 4 < sizeof(ranges) && + strcmp(ranges + n - 4, ".dll") == 0) { + memcpy(ranges + n - 4, ".ranges", 8); + remove(ranges); + } +#else + (void)dll_path; +#endif +} + +#ifndef _WIN32 +typedef struct PosixAbiSweep { + int purged; + int kept; +} PosixAbiSweep; + +static int posix_abi_sweep_file(const PsxOverlayCacheFile *file, void *opaque) { + PosixAbiSweep *sweep = (PosixAbiSweep *)opaque; + char error[256] = {0}; + void *handle = psx_overlay_posix_library_open(file->path, error, sizeof(error)); + int abi = 0; + if (handle) { + typedef int (*AbiFn)(void); + AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi"); + abi = abi_fn ? abi_fn() : 0; + psx_overlay_posix_library_close(handle); + } + if (handle && abi == PSX_OVERLAY_ABI_TAG) { + sweep->kept++; + return 0; + } + + if (handle) + loader_log("ABI preflight rejecting %s: dll=0x%X runtime=0x%X", + file->path, abi, PSX_OVERLAY_ABI_TAG); + else + loader_log("ABI preflight rejecting unloadable %s: %s", + file->path, error); + remove_posix_dll_and_manifest(file->path); + cache_idx_remove_path(file->path); + sweep->purged++; + return 0; +} +#endif + /* ---- ABI pre-flight sweep (batch purge) ---------------------------------- * A contract-ABI bump (e.g. v9 -> v10) invalidates EVERY cached DLL at once. * The lazy path handled that per-dispatch: try_load_region -> LoadLibrary -> @@ -1225,7 +1309,18 @@ static void abi_preflight_sweep(const char *dir) { FILE_ATTRIBUTE_NORMAL, NULL); if (m != INVALID_HANDLE_VALUE) CloseHandle(m); #else - (void)dir; + char marker[900]; + snprintf(marker, sizeof marker, "%s/.abi_%08x.ok", dir, + (unsigned)PSX_OVERLAY_ABI_TAG); + if (access(marker, F_OK) == 0) return; + + PosixAbiSweep sweep = {0}; + psx_overlay_posix_scan_cache_dir(dir, posix_abi_sweep_file, &sweep); + if (sweep.purged) + loader_log("abi preflight: purged %d stale DLL(s), kept %d in %s", + sweep.purged, sweep.kept, dir); + FILE *m = fopen(marker, "wb"); + if (m) fclose(m); #endif } @@ -1833,34 +1928,60 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll } #else static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) { - (void)man; (void)man_n; (void)dll; - void *h = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL); - if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); return 0; } + char error[256] = {0}; + void *h = psx_overlay_posix_library_open(dll_path, error, sizeof(error)); + if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, error); return 0; } /* ABI gate (see the _WIN32 branch). */ typedef int (*AbiFn)(void); - AbiFn abi_fn = (AbiFn)dlsym(h, "overlay_abi"); + AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(h, "overlay_abi"); int abi = abi_fn ? abi_fn() : 0; if (abi != PSX_OVERLAY_ABI_TAG) { loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting " "and deleting stale cache entry", dll_path, abi, PSX_OVERLAY_ABI_TAG); - dlclose(h); - remove(dll_path); + psx_overlay_posix_library_close(h); + remove_posix_dll_and_manifest(dll_path); return 0; } typedef void (*InitFn)(const OverlayCallbacks *); - InitFn init_fn = (InitFn)dlsym(h, "overlay_init"); - if (!init_fn) { loader_log("no overlay_init in %s", dll_path); dlclose(h); return 0; } + InitFn init_fn = (InitFn)psx_overlay_posix_library_symbol(h, "overlay_init"); + if (!init_fn) { + loader_log("no overlay_init in %s", dll_path); + psx_overlay_posix_library_close(h); + return 0; + } init_fn(&s_callbacks); - OverlayFlushFn flush_fn = (OverlayFlushFn)dlsym(h, "overlay_flush_cycles"); + OverlayFlushFn flush_fn = (OverlayFlushFn)psx_overlay_posix_library_symbol( + h, "overlay_flush_cycles"); if (!flush_fn || dll < 0 || dll >= (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0]))) { loader_log("no ABI-v11 cycle flush export in %s", dll_path); - dlclose(h); + psx_overlay_posix_library_close(h); return 0; } s_dll_flush[dll] = flush_fn; - loader_log("%s loaded (posix export scan TODO)", dll_path); - return 0; + + /* ELF/Mach-O do not expose a portable export-table walker. The range + * manifest is already the loader's authority, so resolve only its exact + * callable entries. Missing symbols stay interpreted rather than acquiring + * a guessed extent. */ + int registered = 0; + for (int i = 0; i < man_n; i++) { + ManFn *m = &man[i]; + if (m->entry == 0 || m->n == 0) continue; + OverlayFn fn = (OverlayFn)psx_overlay_posix_library_entry(h, m->entry); + if (!fn) continue; + cand_register(m->entry & 0x1FFFFFFFu, fn, m, dll); + registered++; + } + loader_log("loaded %s -> %d manifest candidates", dll_path, registered); + if (registered == 0) { + s_dll_flush[dll] = NULL; + psx_overlay_posix_library_close(h); + } + /* A successful handle intentionally stays open for process lifetime: the + * registered function and flush pointers refer into it. Rescans are + * idempotent through s_loaded_paths and never acquire a second reference. */ + return registered; } #endif diff --git a/runtime/src/overlay_posix.c b/runtime/src/overlay_posix.c new file mode 100644 index 00000000..efe6396a --- /dev/null +++ b/runtime/src/overlay_posix.c @@ -0,0 +1,161 @@ +#include "overlay_posix.h" + +#include +#include + +static int hex_nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static int parse_hex8(const char *text, uint32_t *value) { + uint32_t parsed = 0; + for (int i = 0; i < 8; i++) { + int nibble = hex_nibble(text[i]); + if (nibble < 0) return 0; + parsed = (parsed << 4) | (uint32_t)nibble; + } + if (value) *value = parsed; + return 1; +} + +int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start, + uint32_t *content_crc) { + uint32_t addr = 0, crc = 0; + if (!name || strlen(name) != 21 || name[8] != '_' || + strcmp(name + 17, ".dll") != 0 || + !parse_hex8(name, &addr) || !parse_hex8(name + 9, &crc)) + return 0; + if (region_start) *region_start = addr; + if (content_crc) *content_crc = crc; + return 1; +} + +#ifndef _WIN32 + +#include +#include +#include + +int psx_overlay_posix_scan_cache_dir(const char *dir, + PsxOverlayCacheVisitor visitor, + void *opaque) { + DIR *dp = opendir(dir); + if (!dp) return 0; + + int visited = 0; + struct dirent *de; + while ((de = readdir(dp)) != NULL) { + PsxOverlayCacheFile file; + memset(&file, 0, sizeof(file)); + if (!psx_overlay_cache_name_parse(de->d_name, &file.region_start, + &file.content_crc)) + continue; + int n = snprintf(file.path, sizeof(file.path), "%s/%s", dir, de->d_name); + if (n < 0 || (size_t)n >= sizeof(file.path)) continue; + memcpy(file.name, de->d_name, sizeof(file.name) - 1); + file.name[sizeof(file.name) - 1] = '\0'; + + struct stat st; + if (stat(file.path, &st) != 0 || !S_ISREG(st.st_mode)) continue; + file.mtime = (uint64_t)st.st_mtime; + visited++; + if (visitor && visitor(&file, opaque)) break; + } + closedir(dp); + return visited; +} + +static int note_first_cache_file(const PsxOverlayCacheFile *file, void *opaque) { + (void)file; + *(int *)opaque = 1; + return 1; +} + +int psx_overlay_posix_find_other_cache_tag(const char *base_dir, + const char *expected_tag, + char *found_tag, + size_t found_tag_size) { + DIR *dp = opendir(base_dir); + if (!dp) return 0; + + int found = 0; + struct dirent *de; + while (!found && (de = readdir(dp)) != NULL) { + if (strncmp(de->d_name, "cg", 2) != 0 || + strcmp(de->d_name, expected_tag) == 0) + continue; + char candidate[768]; + int n = snprintf(candidate, sizeof(candidate), "%s/%s", base_dir, + de->d_name); + if (n < 0 || (size_t)n >= sizeof(candidate)) continue; + int has_cache_file = 0; + psx_overlay_posix_scan_cache_dir(candidate, note_first_cache_file, + &has_cache_file); + if (!has_cache_file) continue; + if (found_tag && found_tag_size) + snprintf(found_tag, found_tag_size, "%s", de->d_name); + found = 1; + } + closedir(dp); + return found; +} + +void *psx_overlay_posix_library_open(const char *path, char *error, + size_t error_size) { + dlerror(); + void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!handle && error && error_size) { + const char *message = dlerror(); + snprintf(error, error_size, "%s", message ? message : "unknown dlopen error"); + } + return handle; +} + +void *psx_overlay_posix_library_symbol(void *handle, const char *name) { + return handle ? dlsym(handle, name) : NULL; +} + +void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) { + char name[32]; + snprintf(name, sizeof(name), "func_%08X", entry); + return psx_overlay_posix_library_symbol(handle, name); +} + +void psx_overlay_posix_library_close(void *handle) { + if (handle) dlclose(handle); +} + +#else + +int psx_overlay_posix_scan_cache_dir(const char *dir, + PsxOverlayCacheVisitor visitor, + void *opaque) { + (void)dir; (void)visitor; (void)opaque; + return 0; +} +int psx_overlay_posix_find_other_cache_tag(const char *base_dir, + const char *expected_tag, + char *found_tag, + size_t found_tag_size) { + (void)base_dir; (void)expected_tag; (void)found_tag; (void)found_tag_size; + return 0; +} +void *psx_overlay_posix_library_open(const char *path, char *error, + size_t error_size) { + (void)path; (void)error; (void)error_size; + return NULL; +} +void *psx_overlay_posix_library_symbol(void *handle, const char *name) { + (void)handle; (void)name; + return NULL; +} +void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) { + (void)handle; (void)entry; + return NULL; +} +void psx_overlay_posix_library_close(void *handle) { (void)handle; } + +#endif diff --git a/runtime/tests/overlay_posix_fixture.c b/runtime/tests/overlay_posix_fixture.c new file mode 100644 index 00000000..df718922 --- /dev/null +++ b/runtime/tests/overlay_posix_fixture.c @@ -0,0 +1,12 @@ +#include + +#if defined(__GNUC__) +#define EXPORT __attribute__((visibility("default"))) +#else +#define EXPORT +#endif + +EXPORT int overlay_abi(void) { return 0x12345678; } +EXPORT void overlay_init(const void *callbacks) { (void)callbacks; } +EXPORT void overlay_flush_cycles(void) {} +EXPORT uint32_t func_80012345(void) { return 0xC0DEF00Du; } diff --git a/runtime/tests/run_overlay_posix_test.sh b/runtime/tests/run_overlay_posix_test.sh new file mode 100644 index 00000000..1a4e7730 --- /dev/null +++ b/runtime/tests/run_overlay_posix_test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/../.." && pwd)" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/psxrecomp-posix-overlay.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT + +cache="$tmp/cache" +base="$tmp/tags" +expected="cg12_11111111" +other="cg12_22222222" +mkdir -p "$cache/00000000_11111111.dll" "$base/$expected" "$base/$other" + +cc -std=c99 -Wall -Wextra -Werror -fPIC -shared \ + "$root/runtime/tests/overlay_posix_fixture.c" \ + -o "$cache/80010000_DEADBEEF.dll" +: > "$cache/00000000_00000000.dll" +: > "$cache/80010000_DEADBEG0.dll" +: > "$base/$other/80020000_12345678.dll" + +cc -std=c99 -Wall -Wextra -Werror \ + -I"$root/runtime/include" \ + "$root/runtime/tests/test_overlay_posix.c" \ + "$root/runtime/src/overlay_posix.c" \ + -ldl -o "$tmp/test_overlay_posix" + +"$tmp/test_overlay_posix" "$cache" "$base" "$expected" "$other" diff --git a/runtime/tests/test_overlay_posix.c b/runtime/tests/test_overlay_posix.c new file mode 100644 index 00000000..dde63008 --- /dev/null +++ b/runtime/tests/test_overlay_posix.c @@ -0,0 +1,79 @@ +#include "overlay_posix.h" + +#include +#include +#include +#include + +typedef struct ScanResult { + int count; + uint32_t addr; + uint32_t crc; +} ScanResult; + +static int record_cache_file(const PsxOverlayCacheFile *file, void *opaque) { + ScanResult *result = (ScanResult *)opaque; + result->count++; + if (strcmp(file->name, "80010000_DEADBEEF.dll") == 0) { + result->addr = file->region_start; + result->crc = file->content_crc; + } + return 0; +} + +static void check_name_parser(void) { + uint32_t addr = 1, crc = 1; + assert(psx_overlay_cache_name_parse("00000000_00000000.dll", &addr, &crc)); + assert(addr == 0 && crc == 0); + assert(psx_overlay_cache_name_parse("89abcdef_ABCDEF01.dll", &addr, &crc)); + assert(addr == 0x89ABCDEFu && crc == 0xABCDEF01u); + + assert(!psx_overlay_cache_name_parse("G0010000_DEADBEEF.dll", &addr, &crc)); + assert(!psx_overlay_cache_name_parse("80010000_DEADBEG0.dll", &addr, &crc)); + assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.so", &addr, &crc)); + assert(!psx_overlay_cache_name_parse("80010000-DEADBEEF.dll", &addr, &crc)); + assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.dll.extra", &addr, &crc)); +} + +int main(int argc, char **argv) { + assert(argc == 5); + check_name_parser(); + + ScanResult result = {0}; + assert(psx_overlay_posix_scan_cache_dir(argv[1], record_cache_file, &result) == 2); + assert(result.count == 2); + assert(result.addr == 0x80010000u); + assert(result.crc == 0xDEADBEEFu); + + char found[64] = {0}; + assert(psx_overlay_posix_find_other_cache_tag(argv[2], argv[3], found, + sizeof(found)) == 1); + assert(strcmp(found, argv[4]) == 0); + + char error[256] = {0}; + void *handle = psx_overlay_posix_library_open(argv[1], error, sizeof(error)); + assert(handle == NULL); /* A directory is not a loadable overlay library. */ + + char fixture[1024]; + snprintf(fixture, sizeof(fixture), "%s/80010000_DEADBEEF.dll", argv[1]); + handle = psx_overlay_posix_library_open(fixture, error, sizeof(error)); + assert(handle != NULL); + typedef int (*AbiFn)(void); + typedef uint32_t (*EntryFn)(void); + AbiFn abi = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi"); + EntryFn entry = (EntryFn)psx_overlay_posix_library_entry(handle, 0x80012345u); + assert(abi && abi() == 0x12345678); + assert(entry && entry() == 0xC0DEF00Du); + assert(psx_overlay_posix_library_entry(handle, 0x80099999u) == NULL); + assert(psx_overlay_posix_library_symbol(handle, "func_80099999") == NULL); + psx_overlay_posix_library_close(handle); + + /* A close followed by a fresh open must remain usable; the production + * loader closes rejected/empty libraries and retains successful handles. */ + handle = psx_overlay_posix_library_open(fixture, error, sizeof(error)); + assert(handle != NULL); + psx_overlay_posix_library_close(handle); + + puts("overlay POSIX discovery tests passed"); + return 0; +} From e7b61c807c9e06684bdfeaa47f740c349d253062 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 21:57:57 -0700 Subject: [PATCH 6/8] recompiler: add guarded config MIPS patches Add alias-aware, opcode-verified game-owned instruction replacements before discovery and code generation. Validate duplicate IDs and physical sites, preserve guarded overlay variants, and cover parser and generated-code behavior with CTest. Co-authored-by: douglasjv --- docs/config_schema.md | 32 +++ .../internal/upstream/guarded_mips_patches.md | 33 +++ recompiler/CMakeLists.txt | 28 ++ recompiler/src/config_loader.cpp | 48 ++++ recompiler/src/config_loader.h | 5 + recompiler/src/main_psx.cpp | 9 + recompiler/src/recompiler_patch.cpp | 93 +++++++ recompiler/src/recompiler_patch.h | 47 ++++ recompiler/tests/recompiler_patch_test.cpp | 239 ++++++++++++++++++ runtime/codegen_hash_sources.cmake | 2 + 10 files changed, 536 insertions(+) create mode 100644 docs/internal/upstream/guarded_mips_patches.md create mode 100644 recompiler/src/recompiler_patch.cpp create mode 100644 recompiler/src/recompiler_patch.h create mode 100644 recompiler/tests/recompiler_patch_test.cpp diff --git a/docs/config_schema.md b/docs/config_schema.md index c7eab4c1..b5f54427 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -86,6 +86,13 @@ bios_thunks = "seeds/tomba_bios_thunks.txt" # game-only out_dir = "generated" # both strict = true # both — currently always true out_stem = "SCPH1001" # optional; overrides the auto-derived stem + +[[recompiler.patch]] +id = "descriptive-policy-name" +address = "0x80012340" +expected = "0x24020002" +replacement = "0x24020001" +note = "Why this game-owned instruction change is required" # optional ``` Output filenames: `/_full.c` and @@ -93,6 +100,31 @@ Output filenames: `/_full.c` and from the `rom`/`exe` file basename with the trailing `.BIN` or `.EXE` stripped (`Path.stem` is NOT used because it mishandles `SCUS_942.36`). +Each `[[recompiler.patch]]` replaces one exact 32-bit MIPS word before function +discovery, control-flow analysis, and normal translation. It is intended for +small, understood game-code changes whose +addresses, opcodes, and policy remain in the game repository. The framework +does not contain title IDs or title-specific addresses. + +- `id`, `address`, `expected`, and `replacement` are required hex/string + fields; `note` is optional. +- IDs are case-sensitive and unique within one config. +- Addresses must be four-byte aligned and are unique by the PSX 29-bit physical + address. Thus `0x00012340`, `0x80012340`, and `0xA0012340` are aliases of one + site and cannot define separate patches. +- Main-EXE generation fails if the word at the target site is not `expected`. + This catches a wrong disc revision or stale patch instead of guessing. +- Captured overlays may place unrelated variants at one virtual address. In + overlay mode, a patch is applied only to a variant whose word is `expected`; + a nonmatching variant is translated unchanged. +- When `--config` and `--ws-config` supply the same byte-identical patch it is + deduplicated. Reusing an ID or physical address for different patch data is + an error. + +Patches are build-time inputs, not runtime memory writes or live toggles. +Regenerate the affected main executable or captured overlays after changing +them. + ## Runtime block Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually) diff --git a/docs/internal/upstream/guarded_mips_patches.md b/docs/internal/upstream/guarded_mips_patches.md new file mode 100644 index 00000000..48062067 --- /dev/null +++ b/docs/internal/upstream/guarded_mips_patches.md @@ -0,0 +1,33 @@ +# Guarded MIPS patch upstream provenance + +This focused implementation was rebuilt for current `mstan/psxrecomp` from +Douglas's declarative-patch work in +[`mstan/psxrecomp` PR #15](https://github.com/mstan/psxrecomp/pull/15), exact +source commit +[`19001c0a383edde439988b93eb2385e2d789d350`](https://github.com/douglasjv/psxrecomp-tweaks/commit/19001c0a383edde439988b93eb2385e2d789d350). +The implementation keeps the original design: game-owned instruction data, +an exact opcode guard, replacement before ordinary MIPS-to-C translation, and +guarded overlay variants. This rebuild applies replacements before discovery +and control-flow analysis as well, so patched branches cannot leave the +generated graph based on stale bytes. + +The upstreaming scope intentionally includes only: + +- the typed `[[recompiler.patch]]` schema and loader; +- physical-alias-aware uniqueness and lookup; +- main-EXE failure on an opcode mismatch; +- overlay nonmatch pass-through; +- focused parser/code-generation tests and schema documentation. + +It intentionally excludes all other work from PR #15 and subsequent Douglas +branches: signed widescreen bounds, textured-edge expansion, full-mirror +rendering, OpenGL context changes, CPU-copied-overlay fallback, debug/input +controls, and every game repository or title-specific address. Those concerns +require independent review and validation before any separate upstreaming. + +Authorship from the source implementation is retained in the integrating +commit with: + +```text +Co-authored-by: douglasjv +``` diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index c146d7e8..4e27e6b8 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.20) project(PSXRecomp C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CTest) add_subdirectory(lib/fmt) @@ -72,6 +73,7 @@ set(PSXRECOMP_GAME_SOURCES src/code_generator.cpp src/annotations.cpp src/config_loader.cpp + src/recompiler_patch.cpp src/main_psx.cpp ) @@ -160,6 +162,7 @@ add_executable(psxrecomp-bios src/function_discovery.cpp src/full_function_emitter.cpp src/config_loader.cpp + src/recompiler_patch.cpp src/main_bios.cpp ) target_include_directories(psxrecomp-bios PRIVATE @@ -206,3 +209,28 @@ target_include_directories(l2_structural_test PRIVATE lib/fmt/include ) target_link_libraries(l2_structural_test PRIVATE fmt rabbitizer) + +if(BUILD_TESTING) + add_executable(recompiler_patch_test + tests/recompiler_patch_test.cpp + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/control_flow.cpp + src/function_analysis.cpp + src/code_generator.cpp + src/annotations.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + ) + target_include_directories(recompiler_patch_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + lib/ELFIO + ) + target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer) + add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) +endif() diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index cd2387b5..c0cab2db 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -709,6 +710,52 @@ GameConfig load_game_config(const fs::path& config_path_in) { out_stem = derive_out_stem(fs::path(exe_field).filename().string()); } + std::vector recompiler_patches; + if (recomp.contains("patch")) { + const auto& patches = toml::find(recomp, "patch"); + std::set seen_ids; + std::set seen_addresses; + for (const auto& item : patches) { + RecompilerPatch patch; + patch.id = toml::find(item, "id"); + patch.address = parse_hex(toml::find(item, "address"), + "recompiler.patch.address"); + patch.expected = parse_hex(toml::find(item, "expected"), + "recompiler.patch.expected"); + patch.replacement = parse_hex( + toml::find(item, "replacement"), + "recompiler.patch.replacement"); + if (item.contains("note")) { + patch.note = toml::find(item, "note"); + } + + if (patch.id.empty()) { + throw std::runtime_error( + "recompiler.patch.id must not be empty"); + } + if ((patch.address & 3u) != 0u) { + throw std::runtime_error(fmt::format( + "{}: recompiler patch '{}' address 0x{:08X} is not " + "instruction-aligned", + config_path.string(), patch.id, patch.address)); + } + if (!seen_ids.insert(patch.id).second) { + throw std::runtime_error(fmt::format( + "{}: duplicate [[recompiler.patch]] id '{}'", + config_path.string(), patch.id)); + } + const uint32_t address_key = + recompiler_patch_address_key(patch.address); + if (!seen_addresses.insert(address_key).second) { + throw std::runtime_error(fmt::format( + "{}: duplicate [[recompiler.patch]] physical address " + "0x{:08X} (KUSEG/KSEG aliases are one site)", + config_path.string(), address_key)); + } + recompiler_patches.push_back(std::move(patch)); + } + } + // Optional [widescreen] block — per-game hooks for the widescreen hack. std::vector ws_sprite_tag_funcs; uint32_t ws_sprite_anchor_addr = 0; @@ -940,6 +987,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*out_dir*/ out_dir, /*strict*/ strict, /*out_stem*/ out_stem, + /*recompiler_patches*/ recompiler_patches, /*runtime*/ parse_runtime_block(cfg, root), /*ws_sprite_tag_funcs*/ ws_sprite_tag_funcs, /*ws_sprite_anchor_addr*/ ws_sprite_anchor_addr, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 9de5fe53..da86f1f8 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -23,6 +23,8 @@ #include #include +#include "recompiler_patch.h" + namespace PSXRecompV4 { // Pad input mode (per player). Replaces the old analog on/off boolean. @@ -387,6 +389,9 @@ struct GameConfig { std::filesystem::path out_dir; bool strict; std::string out_stem; // derived if not explicit + // Game-owned, exact MIPS word replacements. IDs and physical instruction + // addresses are unique within a config; see docs/config_schema.md. + std::vector recompiler_patches; // [runtime] block (optional) RuntimeConfig runtime; diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index c7f5b811..994959b9 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -129,6 +129,7 @@ int main(int argc, char** argv) { std::set ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x std::set persist_init_sites; // [persist_options] init-store hooks (game_options.toml) + std::vector instruction_patches; bool ws_auto_backdrop_preload = false; // [widescreen.cull] auto_backdrop uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0, ws_bg2d_startx_site = 0, @@ -145,6 +146,7 @@ int main(int argc, char** argv) { extra_funcs_storage = cfg.seeds_path.string(); extra_funcs_path = extra_funcs_storage.c_str(); out_dir = cfg.out_dir; + instruction_patches = cfg.recompiler_patches; ws_tag_funcs.insert(cfg.ws_sprite_tag_funcs.begin(), cfg.ws_sprite_tag_funcs.end()); ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end()); @@ -237,6 +239,8 @@ int main(int argc, char** argv) { ws_backdrop_unsquash.insert(wscfg.ws_backdrop_unsquash_funcs.begin(), wscfg.ws_backdrop_unsquash_funcs.end()); ws_auto_screen_x_cull = ws_auto_screen_x_cull || wscfg.ws_auto_screen_x_cull; ws_auto_backdrop_preload = ws_auto_backdrop_preload || wscfg.ws_auto_backdrop_preload; + PSXRecompV4::merge_recompiler_patches( + instruction_patches, wscfg.recompiler_patches); if (wscfg.ws_bg2d_init_func) ws_bg2d_init_func = wscfg.ws_bg2d_init_func; fmt::print("ws-config: {} (backdrop_x sites={}, unsquash funcs={})\n", ws_config_path.string(), ws_backdrop_x.size(), ws_backdrop_unsquash.size()); @@ -253,6 +257,11 @@ int main(int argc, char** argv) { return 1; } + // Patch the parsed image before discovery/CFG analysis so replacements of + // control-flow instructions cannot disagree with the generated graph. + PSXRecompV4::apply_recompiler_patches_to_executable( + *exe, instruction_patches, overlay_mode); + fmt::print("✓ Successfully parsed PS1-EXE!\n\n"); // Print header information diff --git a/recompiler/src/recompiler_patch.cpp b/recompiler/src/recompiler_patch.cpp new file mode 100644 index 00000000..bc833f02 --- /dev/null +++ b/recompiler/src/recompiler_patch.cpp @@ -0,0 +1,93 @@ +#include "recompiler_patch.h" + +#include + +#include "fmt/format.h" +#include "ps1_exe_parser.h" + +namespace PSXRecompV4 { + +uint32_t recompiler_patch_address_key(uint32_t address) { + return address & 0x1FFFFFFFu; +} + +uint32_t apply_recompiler_patch(std::span patches, + uint32_t address, + uint32_t observed, + bool overlay_mode) { + const uint32_t address_key = recompiler_patch_address_key(address); + for (const auto& patch : patches) { + if (recompiler_patch_address_key(patch.address) != address_key) continue; + if (observed == patch.expected) return patch.replacement; + if (overlay_mode) return observed; + throw std::runtime_error(fmt::format( + "recompiler patch '{}' expected 0x{:08X} at 0x{:08X}, found " + "0x{:08X}; wrong game revision or stale patch", + patch.id, patch.expected, address, observed)); + } + return observed; +} + +void apply_recompiler_patches_to_executable( + PSXRecomp::PS1Executable& executable, + std::span patches, + bool overlay_mode) { + const uint32_t load_key = + recompiler_patch_address_key(executable.load_address()); + const uint64_t image_end = + static_cast(load_key) + executable.code_data.size(); + + for (const auto& patch : patches) { + const uint32_t patch_key = recompiler_patch_address_key(patch.address); + if (patch_key < load_key || static_cast(patch_key) + 4u > image_end) { + continue; // The patch may target a different captured overlay image. + } + + const size_t offset = static_cast(patch_key - load_key); + const uint32_t observed = + static_cast(executable.code_data[offset]) | + (static_cast(executable.code_data[offset + 1]) << 8) | + (static_cast(executable.code_data[offset + 2]) << 16) | + (static_cast(executable.code_data[offset + 3]) << 24); + const uint32_t resolved = apply_recompiler_patch( + std::span(&patch, 1), patch.address, + observed, overlay_mode); + if (resolved == observed) continue; + + executable.code_data[offset] = static_cast(resolved); + executable.code_data[offset + 1] = static_cast(resolved >> 8); + executable.code_data[offset + 2] = static_cast(resolved >> 16); + executable.code_data[offset + 3] = static_cast(resolved >> 24); + } +} + +void merge_recompiler_patches(std::vector& destination, + std::span incoming) { + for (const auto& candidate : incoming) { + bool repeated = false; + for (const auto& existing : destination) { + const bool same_id = existing.id == candidate.id; + const bool same_address = + recompiler_patch_address_key(existing.address) == + recompiler_patch_address_key(candidate.address); + if (!same_id && !same_address) continue; + + const bool identical = same_id && same_address && + existing.expected == candidate.expected && + existing.replacement == candidate.replacement && + existing.note == candidate.note; + if (!identical) { + throw std::runtime_error(fmt::format( + "conflicting recompiler patches '{}' at 0x{:08X} and '{}' " + "at 0x{:08X}", + existing.id, existing.address, candidate.id, + candidate.address)); + } + repeated = true; + break; + } + if (!repeated) destination.push_back(candidate); + } +} + +} // namespace PSXRecompV4 diff --git a/recompiler/src/recompiler_patch.h b/recompiler/src/recompiler_patch.h new file mode 100644 index 00000000..2d38472e --- /dev/null +++ b/recompiler/src/recompiler_patch.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +namespace PSXRecomp { +class PS1Executable; +} + +namespace PSXRecompV4 { + +// One game-owned, opcode-verified instruction replacement. Addresses use PSX +// virtual-address spelling in TOML but compare by their 29-bit physical key, so +// KUSEG/KSEG0/KSEG1 aliases refer to the same instruction site. +struct RecompilerPatch { + std::string id; + uint32_t address = 0; + uint32_t expected = 0; + uint32_t replacement = 0; + std::string note; +}; + +uint32_t recompiler_patch_address_key(uint32_t address); + +// Apply the patch targeting address, if any. A main-EXE guard mismatch throws; +// an overlay mismatch is an expected different variant and leaves the observed +// instruction unchanged. +uint32_t apply_recompiler_patch(std::span patches, + uint32_t address, + uint32_t observed, + bool overlay_mode); + +// Apply every in-range patch directly to the parsed image before discovery and +// CFG analysis. This keeps control-flow replacements and emitted code in sync. +void apply_recompiler_patches_to_executable( + PSXRecomp::PS1Executable& executable, + std::span patches, + bool overlay_mode); + +// Merge patches received through --config and --ws-config. Byte-identical +// repeats are deduplicated; conflicting IDs or aliased addresses throw. +void merge_recompiler_patches(std::vector& destination, + std::span incoming); + +} // namespace PSXRecompV4 diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp new file mode 100644 index 00000000..ada4c101 --- /dev/null +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "code_generator.h" +#include "config_loader.h" +#include "control_flow.h" +#include "fmt/format.h" +#include "recompiler_patch.h" + +namespace fs = std::filesystem; +using PSXRecompV4::RecompilerPatch; + +namespace { + +int failures = 0; + +void check(bool condition, const std::string& name) { + if (condition) { + fmt::print("PASS {}\n", name); + } else { + fmt::print(stderr, "FAIL {}\n", name); + ++failures; + } +} + +template +void check_throws(Fn&& fn, const std::string& needle, + const std::string& name) { + try { + fn(); + check(false, name + " (did not throw)"); + } catch (const std::exception& e) { + check(std::string(e.what()).find(needle) != std::string::npos, + name + " (" + e.what() + ")"); + } +} + +void append_word(std::vector& bytes, uint32_t word) { + bytes.push_back(static_cast(word)); + bytes.push_back(static_cast(word >> 8)); + bytes.push_back(static_cast(word >> 16)); + bytes.push_back(static_cast(word >> 24)); +} + +std::string generate_first_instruction(uint32_t first_word, + std::vector patches, + bool overlay_mode) { + constexpr uint32_t base = 0x80010000u; + PSXRecomp::PS1Executable exe{}; + exe.header.load_address = base; + exe.header.initial_pc = base; + exe.header.file_size = 20; + append_word(exe.code_data, first_word); + append_word(exe.code_data, 0x00000000u); // branch/nonbranch delay-slot nop + append_word(exe.code_data, 0x24030001u); // addiu v1, zero, 1 + append_word(exe.code_data, 0x03E00008u); // jr ra + append_word(exe.code_data, 0x00000000u); // return delay-slot nop + + PSXRecompV4::apply_recompiler_patches_to_executable( + exe, patches, overlay_mode); + + PSXRecomp::Function function{}; + function.start_addr = base; + function.end_addr = base + 20; + function.size = 20; + function.name = "patch_test"; + + PSXRecomp::ControlFlowAnalyzer analyzer(exe); + const auto cfg = analyzer.analyze_function(function); + PSXRecomp::CodeGenConfig config; + config.overlay_mode = overlay_mode; + PSXRecomp::CodeGenerator generator(exe, config); + return generator.generate_function(function, cfg).full_code; +} + +std::string base_config() { + return R"toml([game] +name = "Patch Test" +id = "TEST-00000" +exe = "TEST.EXE" +load_address = "0x80010000" +entry_pc = "0x80010000" +text_size = "0x1000" +stack_base = "0x801FFFF0" + +[recompiler] +seeds = "seeds.txt" +out_dir = "generated" +)toml"; +} + +fs::path write_config(const fs::path& root, const std::string& suffix, + const std::string& patch_tables) { + const fs::path path = root / ("game-" + suffix + ".toml"); + std::ofstream file(path, std::ios::binary); + file << base_config() << patch_tables; + file.close(); + return path; +} + +void parser_tests(const fs::path& root) { + const auto valid = write_config(root, "valid", R"toml( +[[recompiler.patch]] +id = "gameplay-rate" +address = "0x80012340" +expected = "0x24020002" +replacement = "0x24020001" +note = "Test-only fixture" +)toml"); + const auto config = PSXRecompV4::load_game_config(valid); + check(config.recompiler_patches.size() == 1, + "parser accepts one guarded patch"); + check(config.recompiler_patches[0].id == "gameplay-rate" && + config.recompiler_patches[0].address == 0x80012340u && + config.recompiler_patches[0].expected == 0x24020002u && + config.recompiler_patches[0].replacement == 0x24020001u, + "parser preserves patch fields"); + + const auto duplicate_address = write_config(root, "duplicate-address", R"toml( +[[recompiler.patch]] +id = "first" +address = "0x80012340" +expected = "0x24020002" +replacement = "0x24020001" + +[[recompiler.patch]] +id = "alias" +address = "0xA0012340" +expected = "0x24020002" +replacement = "0x24020001" +)toml"); + check_throws( + [&] { (void)PSXRecompV4::load_game_config(duplicate_address); }, + "physical address", "parser rejects duplicate aliased addresses"); + + const auto duplicate_id = write_config(root, "duplicate-id", R"toml( +[[recompiler.patch]] +id = "same-id" +address = "0x80012340" +expected = "0x24020002" +replacement = "0x24020001" + +[[recompiler.patch]] +id = "same-id" +address = "0x80012344" +expected = "0x24030002" +replacement = "0x24030001" +)toml"); + check_throws([&] { (void)PSXRecompV4::load_game_config(duplicate_id); }, + "duplicate [[recompiler.patch]] id", + "parser rejects duplicate IDs"); + + const auto unaligned = write_config(root, "unaligned", R"toml( +[[recompiler.patch]] +id = "unaligned" +address = "0x80012342" +expected = "0x24020002" +replacement = "0x24020001" +)toml"); + check_throws([&] { (void)PSXRecompV4::load_game_config(unaligned); }, + "instruction-aligned", "parser rejects unaligned sites"); +} + +void codegen_tests() { + constexpr uint32_t original = 0x24020002u; // addiu v0, zero, 2 + constexpr uint32_t replacement = 0x24020001u; // addiu v0, zero, 1 + const RecompilerPatch physical_alias{ + "gameplay-rate", 0x00010000u, original, replacement, ""}; + + const std::string applied = + generate_first_instruction(original, {physical_alias}, false); + check(applied.find("0x24020001") != std::string::npos && + applied.find("cpu->gpr[2] = 1;") != std::string::npos, + "codegen applies exact patch through physical alias"); + + check_throws( + [&] { (void)generate_first_instruction(0x24020003u, + {physical_alias}, false); }, + "wrong game revision or stale patch", + "main codegen fails on stale opcode guard"); + + const std::string overlay = + generate_first_instruction(0x24020003u, {physical_alias}, true); + check(overlay.find("0x24020003") != std::string::npos && + overlay.find("0x24020001") == std::string::npos, + "overlay nonmatching variant remains unchanged"); + + constexpr uint32_t beq_v0_zero = 0x10400002u; + constexpr uint32_t bne_v0_zero = 0x14400002u; + const RecompilerPatch branch_patch{ + "feature-branch", 0x80010000u, beq_v0_zero, bne_v0_zero, ""}; + const std::string branch = + generate_first_instruction(beq_v0_zero, {branch_patch}, false); + check(branch.find("cpu->gpr[2] != cpu->gpr[0]") != std::string::npos, + "patch is applied before control-flow analysis"); + + std::vector merged{physical_alias}; + PSXRecompV4::merge_recompiler_patches(merged, {&physical_alias, 1}); + check(merged.size() == 1, + "config merge deduplicates an identical patch"); + + const RecompilerPatch conflicting_id{ + "gameplay-rate", 0x00010004u, original, replacement, ""}; + check_throws( + [&] { + PSXRecompV4::merge_recompiler_patches( + merged, {&conflicting_id, 1}); + }, + "conflicting recompiler patches", + "config merge rejects cross-config ID conflicts"); +} + +} // namespace + +int main() { + const fs::path root = fs::temp_directory_path() / + fmt::format("psxrecomp-patch-test-{}", reinterpret_cast(&failures)); + fs::remove_all(root); + fs::create_directories(root); + + try { + parser_tests(root); + codegen_tests(); + } catch (const std::exception& e) { + fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what()); + ++failures; + } + + std::error_code ec; + fs::remove_all(root, ec); + fmt::print("\nGuarded recompiler patches: {}\n", + failures == 0 ? "all tests passed" : "failures detected"); + return failures == 0 ? 0 : 1; +} diff --git a/runtime/codegen_hash_sources.cmake b/runtime/codegen_hash_sources.cmake index 19f4dd3d..d9aaa32c 100644 --- a/runtime/codegen_hash_sources.cmake +++ b/runtime/codegen_hash_sources.cmake @@ -22,6 +22,8 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/control_flow.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/basic_block.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_analysis.cpp + ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/recompiler_patch.cpp + ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/recompiler_patch.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.cpp From 2f73f73f921dc7b9c84aeec57c400903ade4bc15 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 22:33:02 -0700 Subject: [PATCH 7/8] fix: keep patch config header C++17-compatible Avoid exposing std::span through config_loader.h, which is consumed by C++17 game runtimes. Add a dedicated C++17 header compile test alongside the guarded-patch suite. --- recompiler/CMakeLists.txt | 11 ++++++ recompiler/src/recompiler_patch.cpp | 34 ++++++++++++------- recompiler/src/recompiler_patch.h | 7 ++-- .../recompiler_patch_cxx17_header_test.cpp | 9 +++++ recompiler/tests/recompiler_patch_test.cpp | 5 ++- 5 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 recompiler/tests/recompiler_patch_cxx17_header_test.cpp diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 4e27e6b8..65cbc2f8 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -233,4 +233,15 @@ if(BUILD_TESTING) ) target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer) add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) + + add_executable(recompiler_patch_cxx17_header_test + tests/recompiler_patch_cxx17_header_test.cpp + ) + target_include_directories(recompiler_patch_cxx17_header_test PRIVATE src) + set_target_properties(recompiler_patch_cxx17_header_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) + add_test(NAME recompiler_patch_cxx17_header_test + COMMAND recompiler_patch_cxx17_header_test) endif() diff --git a/recompiler/src/recompiler_patch.cpp b/recompiler/src/recompiler_patch.cpp index bc833f02..f6741509 100644 --- a/recompiler/src/recompiler_patch.cpp +++ b/recompiler/src/recompiler_patch.cpp @@ -7,30 +7,41 @@ namespace PSXRecompV4 { +namespace { + +uint32_t apply_one_recompiler_patch(const RecompilerPatch& patch, + uint32_t address, + uint32_t observed, + bool overlay_mode) { + if (observed == patch.expected) return patch.replacement; + if (overlay_mode) return observed; + throw std::runtime_error(fmt::format( + "recompiler patch '{}' expected 0x{:08X} at 0x{:08X}, found " + "0x{:08X}; wrong game revision or stale patch", + patch.id, patch.expected, address, observed)); +} + +} // namespace + uint32_t recompiler_patch_address_key(uint32_t address) { return address & 0x1FFFFFFFu; } -uint32_t apply_recompiler_patch(std::span patches, +uint32_t apply_recompiler_patch(const std::vector& patches, uint32_t address, uint32_t observed, bool overlay_mode) { const uint32_t address_key = recompiler_patch_address_key(address); for (const auto& patch : patches) { if (recompiler_patch_address_key(patch.address) != address_key) continue; - if (observed == patch.expected) return patch.replacement; - if (overlay_mode) return observed; - throw std::runtime_error(fmt::format( - "recompiler patch '{}' expected 0x{:08X} at 0x{:08X}, found " - "0x{:08X}; wrong game revision or stale patch", - patch.id, patch.expected, address, observed)); + return apply_one_recompiler_patch(patch, address, observed, overlay_mode); } return observed; } void apply_recompiler_patches_to_executable( PSXRecomp::PS1Executable& executable, - std::span patches, + const std::vector& patches, bool overlay_mode) { const uint32_t load_key = recompiler_patch_address_key(executable.load_address()); @@ -49,9 +60,8 @@ void apply_recompiler_patches_to_executable( (static_cast(executable.code_data[offset + 1]) << 8) | (static_cast(executable.code_data[offset + 2]) << 16) | (static_cast(executable.code_data[offset + 3]) << 24); - const uint32_t resolved = apply_recompiler_patch( - std::span(&patch, 1), patch.address, - observed, overlay_mode); + const uint32_t resolved = apply_one_recompiler_patch( + patch, patch.address, observed, overlay_mode); if (resolved == observed) continue; executable.code_data[offset] = static_cast(resolved); @@ -62,7 +72,7 @@ void apply_recompiler_patches_to_executable( } void merge_recompiler_patches(std::vector& destination, - std::span incoming) { + const std::vector& incoming) { for (const auto& candidate : incoming) { bool repeated = false; for (const auto& existing : destination) { diff --git a/recompiler/src/recompiler_patch.h b/recompiler/src/recompiler_patch.h index 2d38472e..afb29cba 100644 --- a/recompiler/src/recompiler_patch.h +++ b/recompiler/src/recompiler_patch.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -27,7 +26,7 @@ uint32_t recompiler_patch_address_key(uint32_t address); // Apply the patch targeting address, if any. A main-EXE guard mismatch throws; // an overlay mismatch is an expected different variant and leaves the observed // instruction unchanged. -uint32_t apply_recompiler_patch(std::span patches, +uint32_t apply_recompiler_patch(const std::vector& patches, uint32_t address, uint32_t observed, bool overlay_mode); @@ -36,12 +35,12 @@ uint32_t apply_recompiler_patch(std::span patches, // CFG analysis. This keeps control-flow replacements and emitted code in sync. void apply_recompiler_patches_to_executable( PSXRecomp::PS1Executable& executable, - std::span patches, + const std::vector& patches, bool overlay_mode); // Merge patches received through --config and --ws-config. Byte-identical // repeats are deduplicated; conflicting IDs or aliased addresses throw. void merge_recompiler_patches(std::vector& destination, - std::span incoming); + const std::vector& incoming); } // namespace PSXRecompV4 diff --git a/recompiler/tests/recompiler_patch_cxx17_header_test.cpp b/recompiler/tests/recompiler_patch_cxx17_header_test.cpp new file mode 100644 index 00000000..70a1ea85 --- /dev/null +++ b/recompiler/tests/recompiler_patch_cxx17_header_test.cpp @@ -0,0 +1,9 @@ +#include "recompiler_patch.h" + +#include + +int main() { + std::vector patches; + patches.push_back({"header-smoke", 0x80010000u, 0u, 0u, {}}); + return patches.front().id == "header-smoke" ? 0 : 1; +} diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index ada4c101..e1546e89 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -200,7 +200,7 @@ void codegen_tests() { "patch is applied before control-flow analysis"); std::vector merged{physical_alias}; - PSXRecompV4::merge_recompiler_patches(merged, {&physical_alias, 1}); + PSXRecompV4::merge_recompiler_patches(merged, {physical_alias}); check(merged.size() == 1, "config merge deduplicates an identical patch"); @@ -208,8 +208,7 @@ void codegen_tests() { "gameplay-rate", 0x00010004u, original, replacement, ""}; check_throws( [&] { - PSXRecompV4::merge_recompiler_patches( - merged, {&conflicting_id, 1}); + PSXRecompV4::merge_recompiler_patches(merged, {conflicting_id}); }, "conflicting recompiler patches", "config merge rejects cross-config ID conflicts"); From 5f62dedaa3bddfdae6903524449215caa0b612f5 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 13 Jul 2026 22:36:32 -0700 Subject: [PATCH 8/8] fix: inline runtime-visible patch address key Runtime config parsing uses the physical alias key but does not link the recompiler patch implementation. Keep the dependency-free key in the C++17-compatible header and exercise it in the header smoke test. --- recompiler/src/recompiler_patch.cpp | 4 ---- recompiler/src/recompiler_patch.h | 4 +++- recompiler/tests/recompiler_patch_cxx17_header_test.cpp | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/recompiler/src/recompiler_patch.cpp b/recompiler/src/recompiler_patch.cpp index f6741509..31b15bb9 100644 --- a/recompiler/src/recompiler_patch.cpp +++ b/recompiler/src/recompiler_patch.cpp @@ -23,10 +23,6 @@ uint32_t apply_one_recompiler_patch(const RecompilerPatch& patch, } // namespace -uint32_t recompiler_patch_address_key(uint32_t address) { - return address & 0x1FFFFFFFu; -} - uint32_t apply_recompiler_patch(const std::vector& patches, uint32_t address, uint32_t observed, diff --git a/recompiler/src/recompiler_patch.h b/recompiler/src/recompiler_patch.h index afb29cba..472af983 100644 --- a/recompiler/src/recompiler_patch.h +++ b/recompiler/src/recompiler_patch.h @@ -21,7 +21,9 @@ struct RecompilerPatch { std::string note; }; -uint32_t recompiler_patch_address_key(uint32_t address); +inline uint32_t recompiler_patch_address_key(uint32_t address) { + return address & 0x1FFFFFFFu; +} // Apply the patch targeting address, if any. A main-EXE guard mismatch throws; // an overlay mismatch is an expected different variant and leaves the observed diff --git a/recompiler/tests/recompiler_patch_cxx17_header_test.cpp b/recompiler/tests/recompiler_patch_cxx17_header_test.cpp index 70a1ea85..048bda43 100644 --- a/recompiler/tests/recompiler_patch_cxx17_header_test.cpp +++ b/recompiler/tests/recompiler_patch_cxx17_header_test.cpp @@ -5,5 +5,9 @@ int main() { std::vector patches; patches.push_back({"header-smoke", 0x80010000u, 0u, 0u, {}}); - return patches.front().id == "header-smoke" ? 0 : 1; + return patches.front().id == "header-smoke" && + PSXRecompV4::recompiler_patch_address_key( + patches.front().address) == 0x00010000u + ? 0 + : 1; }