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] 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()