From 5d7837e99119152013ce350c7db3554077b67777 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 15:38:34 -0700 Subject: [PATCH 1/5] fix(runtime): restore capture guards after nonlocal guest escapes --- docs/TCP_COMMANDS.md | 2 ++ runtime/include/interrupts.h | 2 ++ runtime/include/mod_runtime.h | 7 +++-- runtime/src/debug_server.c | 13 +++++++++ runtime/src/interrupts.c | 12 +++++++++ runtime/src/mod_runtime.cpp | 4 +++ runtime/src/render_pass.c | 14 +++++++++- runtime/src/traps.c | 5 ++++ runtime/tests/test_host_snapshot_guard.c | 11 ++++++++ runtime/tests/test_mod_runtime.cpp | 34 ++++++++++++++++++++++++ runtime/tests/test_render_pass_abort.c | 16 +++++++++++ runtime/tests/test_render_pass_sandbox.c | 3 +++ 12 files changed, 120 insertions(+), 3 deletions(-) diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index c9028a2f..0dd152cd 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -79,6 +79,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Debug builds also accept `pad_type` (0 digital, 1 DualShock, 2 JogCon) to test an emulated device identity. Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames. Runtime: `layer="host"` instead arms a virtual P1 gamepad (`buttons`, `lx`..`ry`, `lt`/`rt` 0-255) that feeds the normal offline input path (controller source, title pad transform, trigger values) and host shortcut polling, also headless; a plain override still wins while armed; `clear_input` disarms it | | `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides | | `rewind_status` | ✓ | | — | Local Rewind: `enabled`, `open`, `title_blocked` (`psx_mod_set_rewind_blocked`) and `snaps` held in the ring | +| `snapshot_status` | ✓ | | — | Shared save/rewind capture guards: `callback_active`, `host_call_depth`, resume `site`/`pc`, and `safe`. A busy boundary can legitimately report unsafe; use `savestate_status` to check whether a requested operation completed | | `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | | `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | | `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | @@ -764,6 +765,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `sio_irq_window` | ✓ | | | | `sio_pc_trace` | ✓ | | | | `sio_pc_window` | ✓ | | | +| `snapshot_status` | ✓ | | | | `sio_state` | ✓ | | ✓ | | `sio_trace` | ✓ | ✓ | | | `sio_trace_reset` | | ✓ | | diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 948d03ae..5c9b151d 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -14,6 +14,8 @@ struct CPUState; void psx_snapshot_host_call_begin(void); void psx_snapshot_host_call_end(void); void psx_snapshot_host_call_reset(void); +unsigned psx_snapshot_host_call_depth(void); +void psx_snapshot_host_call_restore(unsigned depth); /* IRQ bit positions in I_STAT/I_MASK */ #define IRQ_VBLANK 0 diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index dd735fca..6c019c0b 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -98,11 +98,14 @@ void mod_runtime_on_savestate_loaded(void); * launcher commit and before renderer/window initialization. */ void mod_runtime_activate_plugins(void); void mod_runtime_on_vblank(void); -/* Host-only context for a nested render transaction. Restore the interrupted - * callback's depth/owner after longjmp; never serialize this into guest saves. */ +/* Host-only callback continuation. Exception/render landings restore the live + * outer callback; scheduler escapes restore an empty context after abandoning + * all callbacks. Never serialize this into guest saves. */ typedef struct ModFunctionEntryContext { uint32_t depth; const void *plugin; + struct CPUState *cpu; + int finished; } ModFunctionEntryContext; void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out); void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index a09352e8..3b472b0f 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9296,6 +9296,18 @@ static void handle_savestate_status(int id, const char *json) send_fmt("{\"id\":%d,\"ok\":true,%s}", id, status); } +static void handle_snapshot_status(int id, const char *json) +{ + (void)json; + extern int psx_mod_function_entry_active(void); + const uint32_t pc = psx_irq_resume_context_snapshot_pc(); + send_fmt("{\"id\":%d,\"ok\":true,\"callback_active\":%s," + "\"host_call_depth\":%u,\"site\":%d,\"pc\":\"0x%08X\",\"safe\":%s}", + id, psx_mod_function_entry_active() ? "true" : "false", + psx_snapshot_host_call_depth(), psx_irq_resume_context_snapshot_site(), + pc, psx_irq_resume_context_snapshot_safe_at(pc) ? "true" : "false"); +} + static void handle_turbo(int id, const char *json) { int enabled = json_get_int(json, "enabled", -1); @@ -15721,6 +15733,7 @@ static const CmdEntry s_commands[] = { { "input_route_status",handle_input_route_status }, { "savestate", handle_savestate }, { "savestate_status", handle_savestate_status }, + { "snapshot_status", handle_snapshot_status }, { "turbo", handle_turbo }, { "turbo_state", handle_turbo_state }, { "pause", handle_pause }, diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index de5bccec..b01c984e 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -26,6 +26,7 @@ */ #include "interrupts.h" +#include "mod_runtime.h" #include "psx_memory.h" #include "sio.h" #include "timers.h" @@ -650,6 +651,8 @@ void psx_snapshot_host_call_end(void) { if (s_snapshot_host_call_depth) --s_snapshot_host_call_depth; } void psx_snapshot_host_call_reset(void) { s_snapshot_host_call_depth = 0; } +unsigned psx_snapshot_host_call_depth(void) { return s_snapshot_host_call_depth; } +void psx_snapshot_host_call_restore(unsigned depth) { s_snapshot_host_call_depth = depth; } uint32_t psx_irq_resume_context_snapshot_pc(void) { @@ -1826,9 +1829,18 @@ irq_deliver_eval: * own live frame. */ jmp_buf saved_exception_jmpbuf; memcpy(&saved_exception_jmpbuf, &exception_jmpbuf, sizeof(jmp_buf)); + ModFunctionEntryContext saved_mod_callback; + mod_runtime_function_entry_context_save(&saved_mod_callback); + const unsigned saved_host_call_depth = psx_snapshot_host_call_depth(); g_exc_setjmp_epoch++; /* new setjmp frame armed (see decl above) */ for (;;) { int jmp_val = setjmp(exception_jmpbuf); + if (jmp_val != 0) { + /* RFE/RestoreState can skip a callback and its guest-call adapter. + * Preserve callbacks below this handler; discard only its scopes. */ + mod_runtime_function_entry_context_restore(&saved_mod_callback); + psx_snapshot_host_call_restore(saved_host_call_depth); + } if (jmp_val == 2) { /* RestoreState redirect: re-dispatch to cpu->pc. * GPRs were already set by RestoreState — do NOT restore. diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index d96bdf5f..c2663e0b 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -3068,12 +3068,16 @@ extern "C" int psx_mod_function_entry_active(void) { extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) { out->depth = PSXRecompV4::function_entry_depth; out->plugin = PSXRecompV4::state().current_plugin; + out->cpu = PSXRecompV4::state().current_function_cpu; + out->finished = PSXRecompV4::state().current_function_finished; } extern "C" void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) { PSXRecompV4::function_entry_depth = in->depth; PSXRecompV4::state().current_plugin = static_cast(in->plugin); + PSXRecompV4::state().current_function_cpu = in->cpu; + PSXRecompV4::state().current_function_finished = in->finished != 0; } /* Apply the committed plan's disc writes and overlays to one sector. The diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index 9a32b917..84d760df 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -30,6 +30,7 @@ #include "gpu_gl_renderer.h" #include "mod_plugins.h" #include "mod_runtime.h" +#include "interrupts.h" #include "overlay_loader.h" #include "psx_icache.h" #include "timers.h" @@ -124,6 +125,7 @@ typedef struct RenderPassNesting { uint32_t ov_inprogress; void (*ov_flush)(void); ModFunctionEntryContext mod_entry; + unsigned snapshot_host_call_depth; PSXModRenderView render_pose; uint32_t span_lo, span_hi; /* open guest span (dirty_ram_run_span) */ } RenderPassNesting; @@ -144,6 +146,7 @@ static void nesting_save(RenderPassNesting *n) { overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress); n->ov_flush = g_overlay_flush_pending_cycles; mod_runtime_function_entry_context_save(&n->mod_entry); + n->snapshot_host_call_depth = psx_snapshot_host_call_depth(); gte_render_pose_get(&n->render_pose); dirty_ram_span_get(&n->span_lo, &n->span_hi); } @@ -164,6 +167,7 @@ static void nesting_restore(const RenderPassNesting *n) { overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress); g_overlay_flush_pending_cycles = n->ov_flush; mod_runtime_function_entry_context_restore(&n->mod_entry); + psx_snapshot_host_call_restore(n->snapshot_host_call_depth); gte_render_pose_set(&n->render_pose); dirty_ram_span_set(n->span_lo, n->span_hi); } @@ -185,7 +189,10 @@ static int nesting_balanced(const RenderPassNesting *n) { now.ov_flush == n->ov_flush && now.span_lo == n->span_lo && now.span_hi == n->span_hi && now.mod_entry.depth == n->mod_entry.depth && - now.mod_entry.plugin == n->mod_entry.plugin; + now.mod_entry.plugin == n->mod_entry.plugin && + now.mod_entry.cpu == n->mod_entry.cpu && + now.mod_entry.finished == n->mod_entry.finished && + now.snapshot_host_call_depth == n->snapshot_host_call_depth; } /* After a watchdog abort, before the restore: which exits the longjmp @@ -223,6 +230,11 @@ static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck, RP_NOTE(now.mod_entry.depth != n->mod_entry.depth, "mod entries %+d", (int)now.mod_entry.depth - (int)n->mod_entry.depth); RP_NOTE(now.mod_entry.plugin != n->mod_entry.plugin, "mod owner"); + RP_NOTE(now.mod_entry.cpu != n->mod_entry.cpu || + now.mod_entry.finished != n->mod_entry.finished, "mod completion context"); + RP_NOTE(now.snapshot_host_call_depth != n->snapshot_host_call_depth, + "host adapter depth %u->%u", n->snapshot_host_call_depth, + now.snapshot_host_call_depth); RP_NOTE(now.span_hi != n->span_hi, "guest span"); #undef RP_NOTE return count; diff --git a/runtime/src/traps.c b/runtime/src/traps.c index e1f1fe98..64a66fec 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -7,6 +7,7 @@ #include "cpu_state.h" #include "psx_memory.h" #include "interrupts.h" +#include "mod_runtime.h" #include "psx_bss.h" #include "psx_runtime.h" /* fix B: psx_exc_escape_reason_t + g_exc_escape_reason */ #include "debug_server.h" @@ -813,6 +814,10 @@ void psx_scheduler_run(CPUState* cpu) for (;;) { if (setjmp(g_scheduler_jmpbuf) != 0) { psx_snapshot_host_call_reset(); + /* The jump also abandoned mod callbacks. Their native epilogues + * cannot clear the snapshot gate or restore callback ownership. */ + const ModFunctionEntryContext no_callback = {0}; + mod_runtime_function_entry_context_restore(&no_callback); /* A structured escape unwound the native stack to here. Reset the * invariants the skipped psx_dispatch_impl frames would otherwise * own. No exception_jmpbuf frame is ever skipped (switch fires only diff --git a/runtime/tests/test_host_snapshot_guard.c b/runtime/tests/test_host_snapshot_guard.c index 50efd514..970ceeef 100644 --- a/runtime/tests/test_host_snapshot_guard.c +++ b/runtime/tests/test_host_snapshot_guard.c @@ -29,6 +29,17 @@ int main(void) { check(psx_irq_resume_context_snapshot_safe_at(pc), "scheduler escape releases abandoned scope"); psx_snapshot_host_call_end(); /* no underflow */ check(psx_irq_resume_context_snapshot_safe_at(pc), "unmatched end stays safe"); + psx_snapshot_host_call_begin(); + const unsigned outer_depth = psx_snapshot_host_call_depth(); + jmp_buf handler; + if (setjmp(handler) == 0) { + psx_snapshot_host_call_begin(); + longjmp(handler, 1); /* handler adapter's cleanup is skipped */ + } + psx_snapshot_host_call_restore(outer_depth); + check(!psx_irq_resume_context_snapshot_safe_at(pc), "exception preserves live outer adapter"); + psx_snapshot_host_call_end(); + check(psx_irq_resume_context_snapshot_safe_at(pc), "outer adapter returns without a leaked gate"); check(!psx_irq_resume_context_snapshot_safe_at(pc + 4), "existing resume-PC guard retained"); g_call_unit_depth = 1; check(!psx_irq_resume_context_snapshot_safe_at(pc), "existing call-unit guard retained"); diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index f1719a4d..36ed5812 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -11,6 +11,7 @@ #include "cpu_state.h" #include +#include #include #include #include @@ -166,6 +167,7 @@ static int disabled_entry_hits; static int unselected_entry_hits; static uint32_t active_entry_last; static int entry_test_mode, nested_result; +static std::jmp_buf callback_escape; static uint32_t nested_pc; static void test_active_entry(CPUState* cpu, uint32_t address) { active_entry_hits++; @@ -190,6 +192,22 @@ static void test_active_entry(CPUState* cpu, uint32_t address) { interrupted_entry_cpu = nullptr; if (!psx_mod_finish_function(cpu)) failures++; cpu->gpr[2] = 0x87654321u; + } else if (entry_test_mode == 5) { + /* Guest scheduling/RFE can abandon the callback's C++ scope. */ + std::longjmp(callback_escape, 1); + } else if (entry_test_mode == 6) { + if (!psx_mod_finish_function(cpu)) failures++; + ModFunctionEntryContext interrupted{}; + mod_runtime_function_entry_context_save(&interrupted); + if (setjmp(callback_escape) == 0) { + CPUState nested{}; + entry_test_mode = 5; + psx_mod_function_entry(&nested, address); + failures++; /* the nested callback must escape */ + } + mod_runtime_function_entry_context_restore(&interrupted); + if (!psx_mod_function_entry_active() || !psx_mod_finish_function(cpu)) failures++; + entry_test_mode = 6; } } static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; } @@ -783,6 +801,22 @@ int main() { "entry completion survives VBlank callbacks during a native call"); entry_test_mode = 0; + entry_test_mode = 5; + if (setjmp(callback_escape) == 0) { + psx_mod_function_entry(&entry_cpu, 0x80003000u); + check(false, "callback must escape without its native cleanup"); + } + check(psx_mod_function_entry_active(), "escape reproduces the stranded capture gate"); + const ModFunctionEntryContext no_callback{}; + mod_runtime_function_entry_context_restore(&no_callback); + check(!psx_mod_function_entry_active() && !psx_mod_finish_function(&entry_cpu), + "scheduler landing releases the gate and clears abandoned completion ownership"); + entry_test_mode = 6; + check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 && + entry_cpu.pc == entry_cpu.gpr[31] && !psx_mod_function_entry_active(), + "nested exception landing preserves the live outer callback and completion"); + entry_test_mode = 0; + entry_cpu.gpr[4] = 35u; const uint32_t guest_sp = entry_cpu.gpr[29]; check(g_psx_mod_guest_functions == 1 && diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index 0b3a3374..1fd3f1ef 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -25,6 +25,9 @@ #include "psx_cyc.h" static ModFunctionEntryContext s_mod_entry; +static unsigned s_host_call_depth; +unsigned psx_snapshot_host_call_depth(void) { return s_host_call_depth; } +void psx_snapshot_host_call_restore(unsigned depth) { s_host_call_depth = depth; } void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) { *out = s_mod_entry; } @@ -251,8 +254,12 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) { uint32_t prev_ov_ip = s_ov_inprogress; void (*prev_flush)(void) = g_overlay_flush_pending_cycles; ModFunctionEntryContext prev_mod = s_mod_entry; + const unsigned prev_host_call = s_host_call_depth; + s_host_call_depth++; s_mod_entry.depth++; s_mod_entry.plugin = f; + s_mod_entry.cpu = cpu; + s_mod_entry.finished = !s_mod_entry.finished; (void)guard; psx_cyc_bb_defer_begin(); g_call_unit_depth = prev_unit + 1; /* overlay_loader_call_native */ @@ -282,6 +289,7 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) { } s_mod_entry = prev_mod; + s_host_call_depth = prev_host_call; s_ld.armed = 0; /* retired on interpreter exit */ g_precise_mode = prev_precise; g_dirty_interp_active = prev_active; @@ -353,6 +361,7 @@ static int stereo_draw(struct CPUState *cpu, void *user, uint32_t eye) { typedef struct Live { ModFunctionEntryContext mod_entry; + unsigned host_call_depth; int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch; int ov_depth; uint32_t ov_ip, batch, resume, span_lo, span_hi; @@ -364,6 +373,7 @@ typedef struct Live { static void snap(Live *l) { memset(l, 0, sizeof *l); l->mod_entry = s_mod_entry; + l->host_call_depth = s_host_call_depth; l->bb_defer = g_psx_cyc_bb_defer; l->unit = g_call_unit_depth; l->dma = g_dma_exec_depth; @@ -395,6 +405,9 @@ static void check_live(const Live *a, const char *when) { CHECK(a->field == b.field, m); } while (0) SAME(mod_entry.depth, "mod callback depth"); SAME(mod_entry.plugin, "mod callback owner"); + SAME(mod_entry.cpu, "mod callback CPU"); + SAME(mod_entry.finished, "mod callback completion"); + SAME(host_call_depth, "host adapter depth"); SAME(bb_defer, "g_psx_cyc_bb_defer"); SAME(unit, "g_call_unit_depth"); SAME(dma, "g_dma_exec_depth"); @@ -442,6 +455,9 @@ int main(void) { * the interpreter's resume latch and a pending load of its own. */ s_mod_entry.depth = 1; s_mod_entry.plugin = &cpu; + s_mod_entry.cpu = &cpu; + s_mod_entry.finished = 1; + s_host_call_depth = 2; g_psx_cyc_bb_defer = 2; g_psx_cyc_batch = 9; g_psx_dispatch_depth = 3; diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 4b4085fd..4248c5f8 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -39,6 +39,9 @@ #include "timers.h" static ModFunctionEntryContext s_mod_entry; +static unsigned s_host_call_depth; +unsigned psx_snapshot_host_call_depth(void) { return s_host_call_depth; } +void psx_snapshot_host_call_restore(unsigned depth) { s_host_call_depth = depth; } void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) { *out = s_mod_entry; } From edb78e8391cb2cbf7f0824d5afd4e4ab82757916 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 16:38:45 -0700 Subject: [PATCH 2/5] Gate snapshots by build-selected implementation identity --- runtime/CMakeLists.txt | 7 +++++++ runtime/include/boot_state.h | 2 +- runtime/src/boot_state.c | 13 ++++++++++--- runtime/tests/test_boot_state_handoff.c | 10 ++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 70786db9..6641373a 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -118,6 +118,13 @@ if(BUILD_TESTING) target_link_options(boot_state_handoff_test PRIVATE -flto -Wl,--gc-sections) target_link_libraries(boot_state_handoff_test PRIVATE ZLIB::ZLIB) add_test(NAME boot_state_handoff_test COMMAND boot_state_handoff_test) + add_executable(boot_state_impl_tag_test tests/test_boot_state_handoff.c) + target_include_directories(boot_state_impl_tag_test PRIVATE include src) + target_compile_definitions(boot_state_impl_tag_test PRIVATE PSX_COSIM=1 PSX_SAVESTATE_IMPL_TAG=0x54474801u) + target_compile_options(boot_state_impl_tag_test PRIVATE -O2 -flto -ffunction-sections -fdata-sections -UNDEBUG) + target_link_options(boot_state_impl_tag_test PRIVATE -flto -Wl,--gc-sections) + target_link_libraries(boot_state_impl_tag_test PRIVATE ZLIB::ZLIB) + add_test(NAME boot_state_impl_tag_test COMMAND boot_state_impl_tag_test) add_executable(static_match_cache_test tests/test_static_match_cache.c src/overlay_loader.c src/crc32.c) diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index 27a94b44..ee25845d 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -66,7 +66,7 @@ typedef struct { uint32_t entry_pc; /* game PS-EXE entry PC */ uint32_t codegen_hash; /* PSX_OVERLAY_CODEGEN_HASH (auto-gen by cmake) */ int32_t abi_tag; /* PSX_OVERLAY_ABI_TAG (abi version | flavor<<16) */ - uint32_t codegen_ver; /* PSX_OVERLAY_CODEGEN_VER */ + uint32_t codegen_ver; /* codegen version XOR build PSX_SAVESTATE_IMPL_TAG */ /* ---- layout ---- */ uint32_t section_count; /* number of sections that follow */ uint32_t reserved; /* v8: enhancement-memory layout cookie; vanilla 0 */ diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index c48e929e..08b731ef 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -2,6 +2,13 @@ #include "fntrace.h" #include "mod_memory.h" #include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */ + +/* Build-selected HLE may use private execution boundaries. Zero preserves + * the existing snapshot key for titles that do not select an implementation. */ +#ifndef PSX_SAVESTATE_IMPL_TAG +#define PSX_SAVESTATE_IMPL_TAG 0u +#endif +#define BOOT_STATE_IMPLEMENTATION_KEY ((uint32_t)PSX_OVERLAY_CODEGEN_VER ^ (uint32_t)PSX_SAVESTATE_IMPL_TAG) #include "dirty_ram_interp.h" #include "gpu.h" /* gpu_get_vram — CPU-auth mirror under dual-raster */ #include "gpu_render.h" /* gr_vram_transfer_in / gr_vram_transfer_out */ @@ -390,7 +397,7 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu, h.entry_pc = entry_pc; h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH; h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG; - h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER; + h.codegen_ver = BOOT_STATE_IMPLEMENTATION_KEY; h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u); ok = write_header_le(o, &h); @@ -870,9 +877,9 @@ int boot_state_check_buffer(const uint8_t* file, size_t file_len, (int)h.abi_tag, (int)PSX_OVERLAY_ABI_TAG); boot_state_append_reason(reason, reason_cap, part); } - if (h.codegen_ver != (uint32_t)PSX_OVERLAY_CODEGEN_VER) { + if (h.codegen_ver != BOOT_STATE_IMPLEMENTATION_KEY) { snprintf(part, sizeof(part), "codegen_ver=%u(want %u)", - (unsigned)h.codegen_ver, (unsigned)PSX_OVERLAY_CODEGEN_VER); + (unsigned)h.codegen_ver, (unsigned)BOOT_STATE_IMPLEMENTATION_KEY); boot_state_append_reason(reason, reason_cap, part); } diff --git a/runtime/tests/test_boot_state_handoff.c b/runtime/tests/test_boot_state_handoff.c index 74040d65..c6493085 100644 --- a/runtime/tests/test_boot_state_handoff.c +++ b/runtime/tests/test_boot_state_handoff.c @@ -68,6 +68,16 @@ int main(void) { /* Malformed, missing and duplicate phase sections must reject before * changing CPU, RAM or the previously active handoff latch. */ cpu.pc=0xdeadbeef; ram[0x100]=0x55; fntrace_restore_game_started(!started); + /* A different build-selected implementation must reject atomically, + * including when its only difference is an HLE execution boundary. */ + PstR key_reader; uint32_t implementation_key; + pst_r_init(&key_reader, saved + 24, 4); + assert(pst_r_u32(&key_reader, &implementation_key)); + assert(implementation_key == BOOT_STATE_IMPLEMENTATION_KEY); + u32(saved + 24, implementation_key ^ 0x54474801u); + assert(!boot_state_load_buffer(saved,len,123,0x801b0000,&cpu)); + assert(cpu.pc==0xdeadbeef && ram[0x100]==0x55 && fntrace_is_game_started()==!started); + u32(saved + 24, implementation_key); saved[len-4]=2; assert(!boot_state_load_buffer(saved,len,123,0x801b0000,&cpu)); saved[len-4]=started; From c6dcdcf834fc075d79b59efbc1b717a21ffa3787 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 16:44:47 -0700 Subject: [PATCH 3/5] Expose snapshot implementation-tag support to title adapters --- runtime/include/boot_state.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index ee25845d..624034a4 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -5,6 +5,10 @@ #include #include "cpu_state.h" +/* Title adapters can require support before selecting HLE execution boundaries. + * PSX_SAVESTATE_IMPL_TAG is a compile definition on boot_state.c, default zero. */ +#define PSX_BOOT_STATE_IMPL_TAG_VERSION 1 + #ifdef __cplusplus extern "C" { #endif From 29e647cebb3bea3cd1ba5d435791ef51911eb783 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 16:58:05 -0700 Subject: [PATCH 4/5] Share build-selected guest HLE bindings and implementation metadata --- docs/GUEST_IMPLEMENTATIONS.md | 57 ++++++++++ runtime/CMakeLists.txt | 5 + runtime/guest_implementation.cmake | 120 +++++++++++++++++++++ runtime/runtime.cmake | 1 + runtime/src/crash_trace.c | 4 + runtime/tests/test_boot_state_handoff.c | 4 + runtime/tests/test_guest_implementation.py | 111 +++++++++++++++++++ 7 files changed, 302 insertions(+) create mode 100644 docs/GUEST_IMPLEMENTATIONS.md create mode 100644 runtime/guest_implementation.cmake create mode 100644 runtime/tests/test_guest_implementation.py diff --git a/docs/GUEST_IMPLEMENTATIONS.md b/docs/GUEST_IMPLEMENTATIONS.md new file mode 100644 index 00000000..80483e58 --- /dev/null +++ b/docs/GUEST_IMPLEMENTATIONS.md @@ -0,0 +1,57 @@ +# Build-selected guest implementations + +`psxrecomp_select_guest_implementation()` binds native HLE replacements to +generated guest functions. Choose HLE or LLE at build time. Both must satisfy +the title's documented caller contract; HLE may remove internal instruction +work and approximate timing. Preserve normal gameplay pacing and a functioning +LLE build. No runtime selector or cross-build state conversion is required. + +After creating the runtime target and its optional PGXP sibling: + +```cmake +set(GEOMETRY_IMPL "HLE" CACHE STRING "HLE or LLE") +set(geometry_targets psx-runtime) +if(TARGET psx-runtime-pgxp) + list(APPEND geometry_targets psx-runtime-pgxp) +endif() +file(GLOB geometry_shards "${CMAKE_CURRENT_SOURCE_DIR}/generated/GAME_full_*.c") +psxrecomp_select_guest_implementation( + NAME geometry IMPLEMENTATION ${GEOMETRY_IMPL} + TARGETS ${geometry_targets} + GENERATED_SOURCES ${geometry_shards} + SYMBOLS func_80000100 func_80000200 + HLE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/geometry_hle.c" + REFERENCE_PREFIX geometry_lle_ + SNAPSHOT_TAG 0x47454F01) +``` + +The HLE source supplies the original function names/signatures. The helper +finds exactly one definition of each symbol, renames that defining shard's +original symbol (for example `geometry_lle_func_80000100`) and leaves dispatch +declarations bound to HLE. The reference bodies can be used in isolated test +builds. Product HLE does not need to execute them. LLE compiles the originals +without renaming or HLE sources. Do not edit generated code to install HLE. + +All targets using the same generated source files must select the same +implementation in that CMake build. Use separate build directories for HLE +and LLE. Call the helper once per routine family, listing every runtime variant. + +Each HLE family needs a stable, distinct, nonzero 32-bit `SNAPSHOT_TAG`. +Change its tag when saved private execution state becomes incompatible. +Selected family tags are XOR-composed into `PSX_SAVESTATE_IMPL_TAG`, which the +snapshot loader checks before applying state. All-LLE uses zero. Memory-card +formats are unaffected. Tags identify compatible execution boundaries rather +than transferring state between implementations. + +The helper records selections in target properties, compile definitions, +startup output and crash-report build identity. The build's CMake cache +records the title's developer-facing selection. Snapshot tests cover tagged +and untagged serialization; `test_guest_implementation.py` compiles and runs +HLE/LLE dispatch for two runtime variants and verifies retained references, +combined family tags, missing symbols and duplicate definitions. + +Shared GTE/device math remains available to native routines. Title-specific +records, pointer ownership, custom register dependencies, animation and packet +layouts belong in the title adapter unless several games establish the same +contract. Tomba's F4/GT3/GT4 geometry batches are the first consumer of this +helper, retaining the complete enhanced-renderer widescreen scene. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 5cc41717..e324baef 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -158,6 +158,11 @@ if(BUILD_TESTING) set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture") find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME guest_implementation_binding_test COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_implementation.py" + --cmake "${CMAKE_COMMAND}" --compiler "${CMAKE_C_COMPILER}" + --generator "${CMAKE_GENERATOR}" + --work-root "${CMAKE_CURRENT_BINARY_DIR}/hle-binding-fixtures") add_test(NAME host_launch_timings_protocol_test COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_C_COMPILER}" "CXX=${CMAKE_CXX_COMPILER}" "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_host_launch_timings_protocol.py") diff --git a/runtime/guest_implementation.cmake b/runtime/guest_implementation.cmake new file mode 100644 index 00000000..d757a6e3 --- /dev/null +++ b/runtime/guest_implementation.cmake @@ -0,0 +1,120 @@ +include_guard(GLOBAL) + +# Bind native guest-function replacements at build time. The original generated +# bodies remain buildable as LLE, and are renamed only in their defining shards +# for HLE reference tests. Dispatch declarations keep the original symbols. +# Every target sharing these generated sources must use the same selection. +function(psxrecomp_select_guest_implementation) + cmake_parse_arguments(IMPL "" "NAME;IMPLEMENTATION;SNAPSHOT_TAG;REFERENCE_PREFIX" + "TARGETS;GENERATED_SOURCES;HLE_SOURCES;SYMBOLS" ${ARGN}) + if(IMPL_UNPARSED_ARGUMENTS OR NOT IMPL_NAME MATCHES "^[A-Za-z_][A-Za-z0-9_]*$") + message(FATAL_ERROR "Guest implementation needs a C-identifier NAME and valid arguments") + endif() + if(NOT IMPL_IMPLEMENTATION MATCHES "^(HLE|LLE)$") + message(FATAL_ERROR "${IMPL_NAME}: IMPLEMENTATION must be HLE or LLE") + endif() + if(NOT IMPL_TARGETS OR NOT IMPL_GENERATED_SOURCES OR NOT IMPL_SYMBOLS) + message(FATAL_ERROR "${IMPL_NAME}: TARGETS, GENERATED_SOURCES and SYMBOLS are required") + endif() + if(NOT IMPL_REFERENCE_PREFIX) + set(IMPL_REFERENCE_PREFIX "psx_lle_${IMPL_NAME}_") + endif() + if(NOT IMPL_REFERENCE_PREFIX MATCHES "^[A-Za-z_][A-Za-z0-9_]*$") + message(FATAL_ERROR "${IMPL_NAME}: invalid REFERENCE_PREFIX") + endif() + set(_tag 0) + if(IMPL_IMPLEMENTATION STREQUAL "HLE") + if(NOT IMPL_HLE_SOURCES OR NOT IMPL_SNAPSHOT_TAG MATCHES "^(0[xX][0-9A-Fa-f]+|[0-9]+)$") + message(FATAL_ERROR "${IMPL_NAME}: HLE needs HLE_SOURCES and a numeric nonzero SNAPSHOT_TAG") + endif() + math(EXPR _tag "${IMPL_SNAPSHOT_TAG}") + if(_tag LESS 1 OR _tag GREATER 4294967295) + message(FATAL_ERROR "${IMPL_NAME}: SNAPSHOT_TAG must fit a nonzero uint32_t") + endif() + endif() + foreach(_symbol IN LISTS IMPL_SYMBOLS) + if(NOT _symbol MATCHES "^[A-Za-z_][A-Za-z0-9_]*$") + message(FATAL_ERROR "${IMPL_NAME}: invalid guest symbol ${_symbol}") + endif() + set(_definition_count_${_symbol} 0) + endforeach() + foreach(_source IN LISTS IMPL_GENERATED_SOURCES) + get_filename_component(_source "${_source}" ABSOLUTE) + if(NOT EXISTS "${_source}") + message(FATAL_ERROR "${IMPL_NAME}: missing generated source ${_source}") + endif() + file(READ "${_source}" _text) + foreach(_symbol IN LISTS IMPL_SYMBOLS) + string(REGEX MATCHALL "(^|\n)[ \t]*void[ \t\r\n]+${_symbol}[ \t\r\n]*\\([^)]*\\)[ \t\r\n]*\\{" _definitions "${_text}") + list(LENGTH _definitions _count) + math(EXPR _definition_count_${_symbol} "${_definition_count_${_symbol}} + ${_count}") + if(_count GREATER 0 AND IMPL_IMPLEMENTATION STREQUAL "HLE") + get_property(_claimed SOURCE "${_source}" PROPERTY "PSX_GUEST_BINDING_${_symbol}") + if(_claimed) + message(FATAL_ERROR "${IMPL_NAME}: ${_symbol} is already bound by ${_claimed}") + endif() + set_property(SOURCE "${_source}" PROPERTY "PSX_GUEST_BINDING_${_symbol}" "${IMPL_NAME}") + set_property(SOURCE "${_source}" APPEND PROPERTY COMPILE_DEFINITIONS + "${_symbol}=${IMPL_REFERENCE_PREFIX}${_symbol}") + endif() + endforeach() + endforeach() + foreach(_symbol IN LISTS IMPL_SYMBOLS) + if(NOT _definition_count_${_symbol} EQUAL 1) + message(FATAL_ERROR "${IMPL_NAME}: expected one definition of ${_symbol}, found ${_definition_count_${_symbol}}") + endif() + endforeach() + + set(_identity "${CMAKE_CURRENT_BINARY_DIR}/guest-implementations/${IMPL_NAME}.c") + file(GENERATE OUTPUT "${_identity}" CONTENT + "#include \"mod_plugins.h\"\n#include \nPSX_MOD_CONSTRUCTOR(psx_impl_${IMPL_NAME}) { fprintf(stdout, \"psxrecomp guest implementation: ${IMPL_NAME}=${IMPL_IMPLEMENTATION}\\n\"); }\n") + foreach(_target IN LISTS IMPL_TARGETS) + if(NOT TARGET ${_target}) + message(FATAL_ERROR "${IMPL_NAME}: unknown target ${_target}") + endif() + get_target_property(_names ${_target} PSX_GUEST_IMPLEMENTATION_NAMES) + if(NOT _names) + set(_names "") + endif() + if(IMPL_NAME IN_LIST _names) + message(FATAL_ERROR "${_target}: duplicate guest implementation ${IMPL_NAME}") + endif() + list(APPEND _names "${IMPL_NAME}") + set_property(TARGET ${_target} PROPERTY PSX_GUEST_IMPLEMENTATION_NAMES "${_names}") + get_target_property(_selections ${_target} PSX_GUEST_IMPLEMENTATIONS) + if(NOT _selections) + set(_selections "") + endif() + list(APPEND _selections "${IMPL_NAME}=${IMPL_IMPLEMENTATION}") + set_property(TARGET ${_target} PROPERTY PSX_GUEST_IMPLEMENTATIONS "${_selections}") + list(JOIN _selections "," _selection_text) + + get_target_property(_tags ${_target} PSX_GUEST_IMPLEMENTATION_TAGS) + if(NOT _tags) + set(_tags "") + endif() + if(_tag GREATER 0 AND _tag IN_LIST _tags) + message(FATAL_ERROR "${_target}: each HLE family needs a distinct SNAPSHOT_TAG") + endif() + list(APPEND _tags "${_tag}") + set_property(TARGET ${_target} PROPERTY PSX_GUEST_IMPLEMENTATION_TAGS "${_tags}") + set(_combined_tag 0) + foreach(_family_tag IN LISTS _tags) + math(EXPR _combined_tag "${_combined_tag} ^ ${_family_tag}") + endforeach() + get_target_property(_definitions ${_target} COMPILE_DEFINITIONS) + if(NOT _definitions) + set(_definitions "") + endif() + list(FILTER _definitions EXCLUDE REGEX "^PSX_(SAVESTATE_IMPL_TAG|BUILD_IMPLEMENTATIONS)=") + set_property(TARGET ${_target} PROPERTY COMPILE_DEFINITIONS "${_definitions}") + target_compile_definitions(${_target} PRIVATE + "PSX_SAVESTATE_IMPL_TAG=${_combined_tag}u" + "PSX_BUILD_IMPLEMENTATIONS=\"${_selection_text}\"") + target_sources(${_target} PRIVATE "${_identity}") + if(IMPL_IMPLEMENTATION STREQUAL "HLE") + target_sources(${_target} PRIVATE ${IMPL_HLE_SOURCES}) + endif() + endforeach() + message(STATUS "psxrecomp guest implementation: ${IMPL_NAME}=${IMPL_IMPLEMENTATION}, tag=${_tag}") +endfunction() diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 422a2789..06ed5efd 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -22,6 +22,7 @@ include("${PSXRECOMP_ROOT}/runtime/openxr_dependency.cmake") include("${PSXRECOMP_ROOT}/runtime/overlay_static_sources.cmake") include("${PSXRECOMP_ROOT}/runtime/netplay_dependency.cmake") include("${PSXRECOMP_ROOT}/runtime/texture_image_dependency.cmake") +include("${PSXRECOMP_ROOT}/runtime/guest_implementation.cmake") # Default to an optimized build. The recompiled game is a huge (~270 MB) block of # generated C; with no CMAKE_BUILD_TYPE the compiler emits it at -O0 and the game diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 16c4874f..96d52cd8 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -55,7 +55,11 @@ static const char *kReportPath = "psx_last_run_report.json"; #ifndef PSX_BUILD_REV #define PSX_BUILD_REV "unknown" #endif +#ifdef PSX_BUILD_IMPLEMENTATIONS +static const char *kBuildId = PSX_BUILD_REV " (" __DATE__ " " __TIME__ ") implementations=" PSX_BUILD_IMPLEMENTATIONS; +#else static const char *kBuildId = PSX_BUILD_REV " (" __DATE__ " " __TIME__ ")"; +#endif /* CPU state pointer (set by debug server at init). */ extern CPUState *debug_cpu_ptr; diff --git a/runtime/tests/test_boot_state_handoff.c b/runtime/tests/test_boot_state_handoff.c index c6493085..c661a052 100644 --- a/runtime/tests/test_boot_state_handoff.c +++ b/runtime/tests/test_boot_state_handoff.c @@ -9,6 +9,10 @@ static uint16_t vram[1024*512]; static int boot_clears, cd_handoffs; uint32_t i_stat, i_mask, g_psx_icache_tv[1024]; uint64_t psx_cycle_count; +uint32_t g_psx_gcs_frac; +uint32_t psx_guest_cycle_scale_config(void) { return 1; } +void psx_guest_cycle_scale_snapshot(uint32_t out[3]) { memset(out,0,12); } +void psx_guest_cycle_scale_restore(const uint32_t in[3]) { (void)in; } uint8_t* memory_get_ram_ptr(void) { return ram; } uint32_t memory_get_ram_bytes(void) { return sizeof ram; } uint8_t* memory_get_scratchpad_ptr(void) { return spad; } diff --git a/runtime/tests/test_guest_implementation.py b/runtime/tests/test_guest_implementation.py new file mode 100644 index 00000000..66be139a --- /dev/null +++ b/runtime/tests/test_guest_implementation.py @@ -0,0 +1,111 @@ +"""Compile/run real CMake bindings, including the retained reference bodies.""" +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cmake', required=True) + parser.add_argument('--compiler', required=True) + parser.add_argument('--generator', default='Ninja') + parser.add_argument('--work-root', type=Path, required=True) + args = parser.parse_args() + framework = Path(__file__).resolve().parents[2] + args.work_root.mkdir(parents=True, exist_ok=True) + root = Path(tempfile.mkdtemp(prefix='binding-', dir=args.work_root)).resolve() + assert root.is_relative_to(args.work_root.resolve()) + + def run(command, ok=True): + result = subprocess.run(command, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + if (result.returncode == 0) != ok: + raise AssertionError(result.stdout) + return result.stdout + + def case(name, implementation, *, duplicate=False, missing=False, second=False): + source = root / name + source.mkdir() + (source / 'cpu.h').write_text('typedef struct { int value; } CPUState;\n') + generated = '#include "cpu.h"\nvoid func_80000100(CPUState* cpu) { cpu->value = 41; }\n' + generated += 'void func_80000200(CPUState* cpu) { cpu->value = 42; }\n' + (source / 'original.c').write_text(generated) + (source / 'dispatch.c').write_text('''#include "cpu.h" +#include +void func_80000100(CPUState*); +int main(void) { + CPUState cpu = {0}; func_80000100(&cpu); + printf("value=%d tag=%u identity=%s\\n", cpu.value, + PSX_SAVESTATE_IMPL_TAG, PSX_BUILD_IMPLEMENTATIONS); + return 0; +} +''') + native = '''#include "cpu.h" +void reference_func_80000100(CPUState*); +void func_80000100(CPUState* cpu) { reference_func_80000100(cpu); cpu->value += 100; } +''' + if second: + native += '''void reference_func_80000200(CPUState*); +void func_80000200(CPUState* cpu) { reference_func_80000200(cpu); cpu->value += 100; } +''' + (source / 'native.c').write_text(native) + sources = '${CMAKE_CURRENT_SOURCE_DIR}/original.c' + if duplicate: + (source / 'duplicate.c').write_text(generated) + sources += ' ${CMAKE_CURRENT_SOURCE_DIR}/duplicate.c' + symbol = 'func_missing' if missing else 'func_80000100' + calls = f'''psxrecomp_select_guest_implementation( + NAME demo IMPLEMENTATION {implementation} + TARGETS base pgxp GENERATED_SOURCES {sources} + HLE_SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/native.c" + SYMBOLS {symbol} REFERENCE_PREFIX reference_ SNAPSHOT_TAG 0x54474801) +''' + if second: + calls += f'''psxrecomp_select_guest_implementation( + NAME other IMPLEMENTATION HLE TARGETS base pgxp + GENERATED_SOURCES {sources} HLE_SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/native.c" + SYMBOLS func_80000200 REFERENCE_PREFIX reference_ SNAPSHOT_TAG 0x00010000) +''' + cmake = f'''cmake_minimum_required(VERSION 3.20) +project(binding C) +include("{framework.as_posix()}/runtime/guest_implementation.cmake") +foreach(target base pgxp) + add_executable(${{target}} original.c dispatch.c) + target_include_directories(${{target}} PRIVATE "{framework.as_posix()}/runtime/include") +endforeach() +{calls}''' + (source / 'CMakeLists.txt').write_text(cmake) + build = source / 'build' + configured = run([args.cmake, '-S', str(source), '-B', str(build), + '-G', args.generator, f'-DCMAKE_C_COMPILER={args.compiler}'], + ok=not (duplicate or missing)) + if duplicate or missing: + assert 'expected one definition' in configured, configured + return + run([args.cmake, '--build', str(build), '--config', 'Release']) + value = 141 if implementation == 'HLE' else 41 + tag = 0x54474801 if implementation == 'HLE' else 0 + identity = f'demo={implementation}' + if second: + tag ^= 0x10000 + identity += ',other=HLE' + for target in ('base', 'pgxp'): + exe = build / (target + ('.exe' if os.name == 'nt' else '')) + if not exe.exists(): + exe = build / 'Release' / exe.name + output = run([str(exe)]) + assert f'value={value} tag={tag} identity={identity}' in output, output + assert f'guest implementation: demo={implementation}' in output, output + + case('lle', 'LLE') + case('hle', 'HLE') + case('multiple-families', 'HLE', second=True) + case('missing-symbol', 'HLE', missing=True) + case('duplicate-definition', 'HLE', duplicate=True) + print('PASS: HLE/LLE dispatch, reference retention, variant identity, composed tags and invalid bindings') + + +if __name__ == '__main__': + main() From 2f69a16370e571057cdba136138bb957a63a2bb7 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 17:00:09 -0700 Subject: [PATCH 5/5] Write guest implementation identity during configure --- runtime/guest_implementation.cmake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/guest_implementation.cmake b/runtime/guest_implementation.cmake index d757a6e3..6b9c43de 100644 --- a/runtime/guest_implementation.cmake +++ b/runtime/guest_implementation.cmake @@ -66,8 +66,9 @@ function(psxrecomp_select_guest_implementation) endforeach() set(_identity "${CMAKE_CURRENT_BINARY_DIR}/guest-implementations/${IMPL_NAME}.c") - file(GENERATE OUTPUT "${_identity}" CONTENT - "#include \"mod_plugins.h\"\n#include \nPSX_MOD_CONSTRUCTOR(psx_impl_${IMPL_NAME}) { fprintf(stdout, \"psxrecomp guest implementation: ${IMPL_NAME}=${IMPL_IMPLEMENTATION}\\n\"); }\n") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/guest-implementations") + file(CONFIGURE OUTPUT "${_identity}" CONTENT + "#include \"mod_plugins.h\"\n#include \nPSX_MOD_CONSTRUCTOR(psx_impl_${IMPL_NAME}) { fprintf(stdout, \"psxrecomp guest implementation: ${IMPL_NAME}=${IMPL_IMPLEMENTATION}\\n\"); }\n" @ONLY) foreach(_target IN LISTS IMPL_TARGETS) if(NOT TARGET ${_target}) message(FATAL_ERROR "${IMPL_NAME}: unknown target ${_target}")