Share build-selected guest HLE bindings and implementation metadata

This commit is contained in:
Matthew Stanley
2026-10-08 16:58:05 -07:00
parent 6fbd5a3462
commit 29e647cebb
7 changed files with 302 additions and 0 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.
+5
View File
@@ -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")
+120
View File
@@ -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 <stdio.h>\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()
+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
+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;
+4
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; }
+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()