Merge pull request #594 from RetroPortingToolKit/fix/tomba-session-capture-20261008

Share build-time guest HLE selection and restore snapshot capture guards
This commit is contained in:
Matt Stanley
2026-10-08 17:02:32 -07:00
committed by GitHub
21 changed files with 455 additions and 7 deletions
+57
View File
@@ -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.
+2
View File
@@ -80,6 +80,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type`, optional `port` | 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. `port=2` (psx-runtime) drives pad 2 instead (digital buttons; plugs a digital pad into port 2 while driven) for headless two-player modes; `press` takes `port` too |
| `clear_input` | ✓ | ✓ | optional `port` | Remove input and analog axis overrides (`port` 1 or 2 clears one port; default both) |
| `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 |
@@ -768,6 +769,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` | | ✓ | |
+12
View File
@@ -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)
@@ -151,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")
+121
View File
@@ -0,0 +1,121 @@
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(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/guest-implementations")
file(CONFIGURE OUTPUT "${_identity}" CONTENT
"#include \"mod_plugins.h\"\n#include <stdio.h>\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}")
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()
+5 -1
View File
@@ -5,6 +5,10 @@
#include <stdint.h>
#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
@@ -66,7 +70,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 */
+2
View File
@@ -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
+5 -2
View File
@@ -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);
+1
View File
@@ -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
+10 -3
View File
@@ -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) +
(psx_guest_cycle_scale_config() != 1u ? 1u : 0u);
@@ -894,9 +901,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);
}
+4
View File
@@ -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;
+13
View File
@@ -9413,6 +9413,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);
@@ -15841,6 +15853,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 },
+12
View File
@@ -26,6 +26,7 @@
*/
#include "interrupts.h"
#include "mod_runtime.h"
#include "psx_memory.h"
#include "sio.h"
#include "timers.h"
@@ -653,6 +654,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)
{
@@ -1829,9 +1832,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.
+4
View File
@@ -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<const PSXRecompV4::ModResolution::Plugin *>(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
+13 -1
View File
@@ -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;
+5
View File
@@ -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
+14
View File
@@ -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; }
@@ -68,6 +72,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;
+111
View File
@@ -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 <stdio.h>
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()
+11
View File
@@ -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");
+34
View File
@@ -11,6 +11,7 @@
#include "cpu_state.h"
#include <array>
#include <csetjmp>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
@@ -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 &&
+16
View File
@@ -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;
+3
View File
@@ -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;
}