From 38bdeeaade3ed3b11998ea2da8773e09bd4e0824 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 15 Aug 2026 19:03:36 -0700 Subject: [PATCH] pgxp: emit hook macros + dual build targets + flavor-namespaced caches (phase 2) Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10): - recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash surface): ONE hook grammar shared by BOTH emitters, wrapping loads/ stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub, or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable sources are captured into locals before the statement. Precision- destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate- on-read drops their stale shadows for free. - code_generator.cpp: central append in translate_instruction; LWC2/SWC2 emissions capture the RAW loaded/stored word (a masked GTE register write must validate against the word as loaded). - strict_translator.cpp: translate() wraps translate_impl()'s c_code with the shared grammar; deferred load-delay variants hook the psx_ldd temp. - BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS; 42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it. - Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 + PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build the sibling target from the same generated tree (framework runtime + game wrapper). No ABI version bump needed: the appended PGXPHooks table is NULL-tolerant and the flavor half of the ABI tag rejects any host/DLL flavor mix. - Shard caches: flavor folded into the cache tag (cg__gc _f) in overlay_loader.c + compile_overlays.py so base and pgxp caches never share a directory; compile_overlays.py passes -DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10. - Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING / PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm from the first vertex); census grew produced/swc2_stores and the pgxp verb reports suppress/active gate state. Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed (engine active, per-vertex lookups flowing; the BIOS shell has no RTPS content so produced=0 there is correct - coverage measurement moves to a 3D title in phase 3). Co-Authored-By: Claude Fable 5 --- recompiler/CMakeLists.txt | 1073 +++++++++-------- recompiler/src/code_generator.cpp | 36 +- recompiler/src/pgxp_hook_emitter.cpp | 134 ++ recompiler/src/pgxp_hook_emitter.h | 31 + recompiler/src/strict_translator.cpp | 59 +- recompiler/src/strict_translator.h | 7 + runtime/CMakeLists.txt | 17 + runtime/codegen_hash_sources.cmake | 2 + runtime/include/overlay_api.h | 20 +- .../include/overlay_dispatch_preamble.c.inc | 23 + runtime/include/pgxp.h | 4 + runtime/runtime.cmake | 54 +- runtime/src/debug_server.c | 12 +- runtime/src/main.cpp | 10 + runtime/src/overlay_loader.c | 33 +- runtime/src/pgxp.cpp | 5 + tools/compile_overlays.py | 8 +- 17 files changed, 941 insertions(+), 587 deletions(-) create mode 100644 recompiler/src/pgxp_hook_emitter.cpp create mode 100644 recompiler/src/pgxp_hook_emitter.h diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index c7639880..71c16cc0 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -1,403 +1,408 @@ -cmake_minimum_required(VERSION 3.20) -project(PSXRecomp C CXX) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -include(CTest) - -include("${CMAKE_CURRENT_SOURCE_DIR}/../runtime/chd_dependency.cmake") - -add_subdirectory(lib/fmt) - -# Vendored fmt 9.1.0 enables its `consteval` format-string check on Apple -# clang via the `__cpp_consteval` clause in core.h, even though the same -# block tries to exclude Apple toolchains — and Apple clang 21 then rejects -# fmt's own literal format strings ("call to consteval function is not a -# constant expression"). Force FMT_CONSTEVAL empty to take fmt's -# non-consteval compile-time-check path. Scoped to non-MSVC so Windows is -# untouched. -if(NOT MSVC) - target_compile_definitions(fmt PUBLIC FMT_CONSTEVAL=) -endif() - -# Rabbitizer MIPS decoder -add_library(rabbitizer STATIC) -target_sources(rabbitizer PRIVATE - lib/rabbitizer/cplusplus/src/analysis/LoPairingInfo.cpp - lib/rabbitizer/cplusplus/src/analysis/RegistersTracker.cpp - lib/rabbitizer/cplusplus/src/instructions/InstrId.cpp - lib/rabbitizer/cplusplus/src/instructions/InstrIdType.cpp - lib/rabbitizer/cplusplus/src/instructions/InstructionBase.cpp - lib/rabbitizer/cplusplus/src/instructions/InstructionCpu.cpp - lib/rabbitizer/cplusplus/src/instructions/InstructionR3000GTE.cpp - lib/rabbitizer/cplusplus/src/instructions/InstructionR5900.cpp - lib/rabbitizer/cplusplus/src/instructions/InstructionRsp.cpp - lib/rabbitizer/src/analysis/RabbitizerLoPairingInfo.c - lib/rabbitizer/src/analysis/RabbitizerRegistersTracker.c - lib/rabbitizer/src/analysis/RabbitizerTrackedRegisterState.c - lib/rabbitizer/src/common/RabbitizerConfig.c - lib/rabbitizer/src/common/RabbitizerVersion.c - lib/rabbitizer/src/common/Utils.c - lib/rabbitizer/src/instructions/RabbitizerInstrCategory.c - lib/rabbitizer/src/instructions/RabbitizerInstrDescriptor.c - lib/rabbitizer/src/instructions/RabbitizerInstrId.c - lib/rabbitizer/src/instructions/RabbitizerInstrIdType.c - lib/rabbitizer/src/instructions/RabbitizerInstrSuffix.c - lib/rabbitizer/src/instructions/RabbitizerInstructionCpu/RabbitizerInstructionCpu_OperandType.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE_OperandType.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE_ProcessUniqueId.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900_OperandType.c - lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900_ProcessUniqueId.c - lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp.c - lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp_OperandType.c - lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp_ProcessUniqueId.c - lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction.c - lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Disassemble.c - lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Examination.c - lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Operand.c - lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_ProcessUniqueId.c - lib/rabbitizer/src/instructions/RabbitizerRegister.c - lib/rabbitizer/src/instructions/RabbitizerRegisterDescriptor.c -) -target_include_directories(rabbitizer PRIVATE - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/rabbitizer/tables -) - -# PSXRecomp — MIPS→C recompiler tool -set(PSXRECOMP_GAME_SOURCES - src/ps1_exe_parser.cpp - src/mips_decoder.cpp - src/control_flow.cpp - src/function_analysis.cpp - src/code_generator.cpp - src/bios_address_model.cpp - src/annotations.cpp - src/config_loader.cpp - src/recompiler_patch.cpp - src/main_psx.cpp -) - -add_executable(psxrecomp-game ${PSXRECOMP_GAME_SOURCES}) -target_include_directories(psxrecomp-game PRIVATE - include - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include - lib/toml11 - lib/ELFIO -) -target_link_libraries(psxrecomp-game PRIVATE fmt rabbitizer) -if(MINGW) - target_link_options(psxrecomp-game PRIVATE -Wl,--stack,16777216) -elseif(MSVC) - target_link_options(psxrecomp-game PRIVATE /STACK:16777216) -endif() - -# psxrecomp-toml — PS-X EXE → game.toml generator. -# Standalone tool that analyses a PS-X EXE and generates a complete game.toml -# config with auto-detected fields (load_address, entry_pc, text_size, -# stack_base) and optional JAL-target seed generation. -add_executable(psxrecomp-toml - src/ps1_exe_parser.cpp - src/main_toml.cpp -) -target_include_directories(psxrecomp-toml PRIVATE - include - lib/fmt/include -) -target_link_libraries(psxrecomp-toml PRIVATE fmt) - -# psxrecomp — user-facing media-to-source project generator. -add_executable(psxrecomp - src/main_cli.cpp - src/cli_boot_path.cpp - src/ps1_exe_parser.cpp - ../runtime/src/cue_sheet.cpp - ../runtime/src/iso_reader.cpp -) -target_include_directories(psxrecomp PRIVATE - include - ../runtime/include - lib/fmt/include -) -target_link_libraries(psxrecomp PRIVATE fmt chdr-static) -if(MINGW) - target_link_options(psxrecomp PRIVATE -Wl,--stack,16777216) -elseif(MSVC) - target_link_options(psxrecomp PRIVATE /STACK:16777216) -endif() - -# ---- baked codegen hash (stale-binary guard) -------------------------------- -# Bake the SAME emitter-source hash the runtime folds into its overlay cache -# tag (runtime/runtime.cmake -> hash_codegen.cmake, shared canonical list in -# runtime/codegen_hash_sources.cmake) into THIS binary, exposed via -# `psxrecomp-game --codegen-hash`. compile_overlays.py compares it against the -# hash it stamps on the cache dir (from --runtime-include) and HARD-FAILS on -# mismatch. Closes the 2026-07-01 Tomba pause-menu wedge class: a stale -# recompiler binary emitting shards that get filed under the CURRENT cg tag -# (read tag == write tag, content stale — invisible to the tag alone). -set(PSXRECOMP_CODEGEN_HASH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) -include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/codegen_hash_sources.cmake) -set(_baked_hash_hdr ${CMAKE_CURRENT_BINARY_DIR}/psxrecomp_baked_codegen_hash.h) -add_custom_command( - OUTPUT ${_baked_hash_hdr} - COMMAND ${CMAKE_COMMAND} -DOUT=${_baked_hash_hdr} - "-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}" - -P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake - DEPENDS ${PSXRECOMP_CODEGEN_HASH_SRCS} - ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake - COMMENT "Hashing codegen sources -> psxrecomp_baked_codegen_hash.h" - VERBATIM) -add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr}) -add_dependencies(psxrecomp-game psxrecomp_baked_codegen_hash) -# Force main_psx.cpp to recompile whenever the hash header content changes -# (same OBJECT_DEPENDS hardening as runtime.cmake — __has_include depfile gap). -set_source_files_properties(src/main_psx.cpp PROPERTIES OBJECT_DEPENDS ${_baked_hash_hdr}) -target_include_directories(psxrecomp-game PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) - -# NOTE (2026-07-27): the legacy `PSXRecomp` target was REMOVED here. -# It built ${PSXRECOMP_GAME_SOURCES} — byte-for-byte the same source list as -# `psxrecomp-game` above — and was a leftover from before that target was -# introduced. Because Windows and macOS filesystems are case-insensitive, its -# output `PSXRecomp.exe` collided with the user-facing CLI `psxrecomp.exe` -# (the `psxrecomp` target above): both wrote the SAME file on disk and the -# last one linked won. In practice the CLI lost, so `psxrecomp.exe` shipped in -# `psxrecomp-cli-windows-x86_64.zip` (tools/build_cli.py) was actually the game -# recompiler, with a completely different argument grammar than the README -# documents. The bug is invisible on Linux, which is why it survived. -# The `PSXRecomp.exe` users are told to run in README's "Release Package" -# section is a DIFFERENT artifact — the BIOS-only runtime, whose name comes -# from OUTPUT_NAME in runtime/runtime.cmake — and is unaffected by this removal. -# See the collision guard at the bottom of this file. - -# psxrecomp-bios — Phase 1a BIOS boot slice recompiler. -# Strict, fail-loud, ingests bios/SCPH1001.BIN as a flat ROM at 0xBFC00000. -# Independent of PSXRecomp's PS1-EXE entry point. Shares mips_decoder + -# ps1_exe_parser sources because mips_decoder.h depends on PS1Executable -# (used only by decode_executable, which psxrecomp-bios does not call). -add_executable(psxrecomp-bios - src/ps1_exe_parser.cpp - src/mips_decoder.cpp - src/strict_translator.cpp - src/bios_slice_walker.cpp - src/bios_address_model.cpp - src/function_discovery.cpp - src/full_function_emitter.cpp - src/config_loader.cpp - src/recompiler_patch.cpp - src/main_bios.cpp -) -target_include_directories(psxrecomp-bios PRIVATE - include - src - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include - lib/toml11 -) -target_link_libraries(psxrecomp-bios PRIVATE fmt rabbitizer) -if(MINGW) - target_link_options(psxrecomp-bios PRIVATE -Wl,--stack,16777216) -elseif(MSVC) - target_link_options(psxrecomp-bios PRIVATE /STACK:16777216) -endif() - -# Released Windows CLI archives must run without a separate MinGW/MSYS2 -# install. Prefer llvm-mingw (+ -static) in CI so emitters do not need -# libgcc/libstdc++ harvested from MSYS2. Keep opt-in for local debug builds. -option(PSXRECOMP_STATIC_CLI "Statically link Windows runtime into CLI tools" OFF) -if(PSXRECOMP_STATIC_CLI AND WIN32 AND (MINGW OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")) - foreach(_cli_target psxrecomp psxrecomp-game psxrecomp-toml psxrecomp-bios) - if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_link_options(${_cli_target} PRIVATE - -static -static-libgcc -static-libstdc++) - else() - # llvm-mingw / Clang: fully static CRT + libc++ / libunwind / pthread. - target_link_options(${_cli_target} PRIVATE -static) - endif() - endforeach() -endif() - -# BIOS address-model equivalence test — sweeps every 4-byte address through -# the profile-driven BiosAddressModel and compares against verbatim copies of -# the pre-refactor hardcoded SCPH1001 helpers; also exercises the from_config -# invariants. Run from the framework root: recompiler/build/bios_address_model_test -add_executable(bios_address_model_test - tests/bios_address_model_test.cpp - src/bios_address_model.cpp - src/config_loader.cpp - src/ps1_exe_parser.cpp -) -target_include_directories(bios_address_model_test PRIVATE - include - src - lib/fmt/include - lib/toml11 -) -target_link_libraries(bios_address_model_test PRIVATE fmt) - -# L1 decoder conformance test — compares MipsDecoder against Rabbitizer -# oracle for every instruction in SCPH1001.BIN code regions. -add_executable(l1_decoder_test - tests/l1_decoder_test.cpp - src/mips_decoder.cpp - src/ps1_exe_parser.cpp -) -target_include_directories(l1_decoder_test PRIVATE - include - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include -) -target_link_libraries(l1_decoder_test PRIVATE fmt rabbitizer) - -# L2 structural codegen test — verifies StrictTranslator output patterns -# for each MIPS instruction form. -add_executable(l2_structural_test - tests/l2_structural_test.cpp - src/mips_decoder.cpp - src/ps1_exe_parser.cpp - src/strict_translator.cpp -) -target_include_directories(l2_structural_test PRIVATE - include - src - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include -) -target_link_libraries(l2_structural_test PRIVATE fmt rabbitizer) - -if(BUILD_TESTING) - add_executable(debug_trace_ranges_test - ../runtime/tests/test_debug_trace_ranges.c - ../runtime/src/debug_trace_ranges.c - ) - target_include_directories(debug_trace_ranges_test PRIVATE - ../runtime/include) - add_test(NAME debug_trace_ranges_test COMMAND debug_trace_ranges_test) - - add_executable(cli_boot_path_test - tests/cli_boot_path_test.cpp - src/cli_boot_path.cpp - ) - target_include_directories(cli_boot_path_test PRIVATE src) - add_test(NAME cli_boot_path_test COMMAND cli_boot_path_test) - - add_executable(recompiler_patch_test - tests/recompiler_patch_test.cpp - src/ps1_exe_parser.cpp - src/mips_decoder.cpp - src/control_flow.cpp - src/function_analysis.cpp - src/code_generator.cpp - src/annotations.cpp - src/bios_address_model.cpp - src/config_loader.cpp - src/recompiler_patch.cpp - ) - target_include_directories(recompiler_patch_test PRIVATE - include - src - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include - lib/toml11 - lib/ELFIO - ) - target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer) - add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) - - add_executable(full_function_emitter_test - tests/full_function_emitter_test.cpp - src/ps1_exe_parser.cpp - src/mips_decoder.cpp - src/strict_translator.cpp - src/bios_address_model.cpp - src/function_discovery.cpp - src/full_function_emitter.cpp - src/config_loader.cpp - src/recompiler_patch.cpp - ) - target_include_directories(full_function_emitter_test PRIVATE - include - src - lib/rabbitizer/include - lib/rabbitizer/cplusplus/include - lib/fmt/include - lib/toml11 - ) - target_link_libraries(full_function_emitter_test PRIVATE fmt rabbitizer) - add_test(NAME full_function_emitter_test COMMAND full_function_emitter_test) - - add_executable(recompiler_patch_cxx17_header_test - tests/recompiler_patch_cxx17_header_test.cpp - ) - target_include_directories(recompiler_patch_cxx17_header_test PRIVATE src) - set_target_properties(recompiler_patch_cxx17_header_test PROPERTIES - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON - ) - add_test(NAME recompiler_patch_cxx17_header_test - COMMAND recompiler_patch_cxx17_header_test) - - # [video] geometry_correction / perspective_texturing opt-in plumbing. - add_executable(video_enhancement_settings_test - tests/video_enhancement_settings_test.cpp - src/config_loader.cpp - src/ps1_exe_parser.cpp - ) - target_include_directories(video_enhancement_settings_test PRIVATE - include - src - lib/fmt/include - lib/toml11 - ) - target_link_libraries(video_enhancement_settings_test PRIVATE fmt) - add_test(NAME video_enhancement_settings_test - COMMAND video_enhancement_settings_test) - - # Needs bios/SCPH1001.toml — run from the framework root. - add_test(NAME bios_address_model_test - COMMAND bios_address_model_test - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - - # These targets were already built by every test build, but without - # add_test() entries ctest could neither run nor fail them. - add_test(NAME l2_structural_test COMMAND l2_structural_test) - add_test(NAME reachable_discovery_test COMMAND reachable_discovery_test) - - find_package(Python3 COMPONENTS Interpreter) - if(Python3_Interpreter_FOUND) - add_test( - NAME cli_project_packaging - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cli_project_packaging.py - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - - # These two invoke psxrecomp-game, whose BIOS-profile resolution - # probes fixed working-directory-relative paths (bios/.toml, - # then psxrecomp/bios/.toml) -- run them from the framework - # root so the shipped profiles are found. - add_test( - NAME overlay_cross_page_delay_codegen - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_cross_page_delay_codegen.py - --recompiler $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - add_test( - NAME fmv_cycle_batch_codegen - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fmv_cycle_batch_codegen.py - --recompiler $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - add_test( - NAME overlay_shim_compile_contract - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_shim_compile.py) +cmake_minimum_required(VERSION 3.20) +project(PSXRecomp C CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CTest) + +include("${CMAKE_CURRENT_SOURCE_DIR}/../runtime/chd_dependency.cmake") + +add_subdirectory(lib/fmt) + +# Vendored fmt 9.1.0 enables its `consteval` format-string check on Apple +# clang via the `__cpp_consteval` clause in core.h, even though the same +# block tries to exclude Apple toolchains — and Apple clang 21 then rejects +# fmt's own literal format strings ("call to consteval function is not a +# constant expression"). Force FMT_CONSTEVAL empty to take fmt's +# non-consteval compile-time-check path. Scoped to non-MSVC so Windows is +# untouched. +if(NOT MSVC) + target_compile_definitions(fmt PUBLIC FMT_CONSTEVAL=) +endif() + +# Rabbitizer MIPS decoder +add_library(rabbitizer STATIC) +target_sources(rabbitizer PRIVATE + lib/rabbitizer/cplusplus/src/analysis/LoPairingInfo.cpp + lib/rabbitizer/cplusplus/src/analysis/RegistersTracker.cpp + lib/rabbitizer/cplusplus/src/instructions/InstrId.cpp + lib/rabbitizer/cplusplus/src/instructions/InstrIdType.cpp + lib/rabbitizer/cplusplus/src/instructions/InstructionBase.cpp + lib/rabbitizer/cplusplus/src/instructions/InstructionCpu.cpp + lib/rabbitizer/cplusplus/src/instructions/InstructionR3000GTE.cpp + lib/rabbitizer/cplusplus/src/instructions/InstructionR5900.cpp + lib/rabbitizer/cplusplus/src/instructions/InstructionRsp.cpp + lib/rabbitizer/src/analysis/RabbitizerLoPairingInfo.c + lib/rabbitizer/src/analysis/RabbitizerRegistersTracker.c + lib/rabbitizer/src/analysis/RabbitizerTrackedRegisterState.c + lib/rabbitizer/src/common/RabbitizerConfig.c + lib/rabbitizer/src/common/RabbitizerVersion.c + lib/rabbitizer/src/common/Utils.c + lib/rabbitizer/src/instructions/RabbitizerInstrCategory.c + lib/rabbitizer/src/instructions/RabbitizerInstrDescriptor.c + lib/rabbitizer/src/instructions/RabbitizerInstrId.c + lib/rabbitizer/src/instructions/RabbitizerInstrIdType.c + lib/rabbitizer/src/instructions/RabbitizerInstrSuffix.c + lib/rabbitizer/src/instructions/RabbitizerInstructionCpu/RabbitizerInstructionCpu_OperandType.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE_OperandType.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR3000GTE/RabbitizerInstructionR3000GTE_ProcessUniqueId.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900_OperandType.c + lib/rabbitizer/src/instructions/RabbitizerInstructionR5900/RabbitizerInstructionR5900_ProcessUniqueId.c + lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp.c + lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp_OperandType.c + lib/rabbitizer/src/instructions/RabbitizerInstructionRsp/RabbitizerInstructionRsp_ProcessUniqueId.c + lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction.c + lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Disassemble.c + lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Examination.c + lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_Operand.c + lib/rabbitizer/src/instructions/RabbitizerInstruction/RabbitizerInstruction_ProcessUniqueId.c + lib/rabbitizer/src/instructions/RabbitizerRegister.c + lib/rabbitizer/src/instructions/RabbitizerRegisterDescriptor.c +) +target_include_directories(rabbitizer PRIVATE + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/rabbitizer/tables +) + +# PSXRecomp — MIPS→C recompiler tool +set(PSXRECOMP_GAME_SOURCES + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/control_flow.cpp + src/function_analysis.cpp + src/code_generator.cpp + src/pgxp_hook_emitter.cpp + src/bios_address_model.cpp + src/annotations.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + src/main_psx.cpp +) + +add_executable(psxrecomp-game ${PSXRECOMP_GAME_SOURCES}) +target_include_directories(psxrecomp-game PRIVATE + include + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + lib/ELFIO +) +target_link_libraries(psxrecomp-game PRIVATE fmt rabbitizer) +if(MINGW) + target_link_options(psxrecomp-game PRIVATE -Wl,--stack,16777216) +elseif(MSVC) + target_link_options(psxrecomp-game PRIVATE /STACK:16777216) +endif() + +# psxrecomp-toml — PS-X EXE → game.toml generator. +# Standalone tool that analyses a PS-X EXE and generates a complete game.toml +# config with auto-detected fields (load_address, entry_pc, text_size, +# stack_base) and optional JAL-target seed generation. +add_executable(psxrecomp-toml + src/ps1_exe_parser.cpp + src/main_toml.cpp +) +target_include_directories(psxrecomp-toml PRIVATE + include + lib/fmt/include +) +target_link_libraries(psxrecomp-toml PRIVATE fmt) + +# psxrecomp — user-facing media-to-source project generator. +add_executable(psxrecomp + src/main_cli.cpp + src/cli_boot_path.cpp + src/ps1_exe_parser.cpp + ../runtime/src/cue_sheet.cpp + ../runtime/src/iso_reader.cpp +) +target_include_directories(psxrecomp PRIVATE + include + ../runtime/include + lib/fmt/include +) +target_link_libraries(psxrecomp PRIVATE fmt chdr-static) +if(MINGW) + target_link_options(psxrecomp PRIVATE -Wl,--stack,16777216) +elseif(MSVC) + target_link_options(psxrecomp PRIVATE /STACK:16777216) +endif() + +# ---- baked codegen hash (stale-binary guard) -------------------------------- +# Bake the SAME emitter-source hash the runtime folds into its overlay cache +# tag (runtime/runtime.cmake -> hash_codegen.cmake, shared canonical list in +# runtime/codegen_hash_sources.cmake) into THIS binary, exposed via +# `psxrecomp-game --codegen-hash`. compile_overlays.py compares it against the +# hash it stamps on the cache dir (from --runtime-include) and HARD-FAILS on +# mismatch. Closes the 2026-07-01 Tomba pause-menu wedge class: a stale +# recompiler binary emitting shards that get filed under the CURRENT cg tag +# (read tag == write tag, content stale — invisible to the tag alone). +set(PSXRECOMP_CODEGEN_HASH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) +include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/codegen_hash_sources.cmake) +set(_baked_hash_hdr ${CMAKE_CURRENT_BINARY_DIR}/psxrecomp_baked_codegen_hash.h) +add_custom_command( + OUTPUT ${_baked_hash_hdr} + COMMAND ${CMAKE_COMMAND} -DOUT=${_baked_hash_hdr} + "-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake + DEPENDS ${PSXRECOMP_CODEGEN_HASH_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake + COMMENT "Hashing codegen sources -> psxrecomp_baked_codegen_hash.h" + VERBATIM) +add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr}) +add_dependencies(psxrecomp-game psxrecomp_baked_codegen_hash) +# Force main_psx.cpp to recompile whenever the hash header content changes +# (same OBJECT_DEPENDS hardening as runtime.cmake — __has_include depfile gap). +set_source_files_properties(src/main_psx.cpp PROPERTIES OBJECT_DEPENDS ${_baked_hash_hdr}) +target_include_directories(psxrecomp-game PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +# NOTE (2026-07-27): the legacy `PSXRecomp` target was REMOVED here. +# It built ${PSXRECOMP_GAME_SOURCES} — byte-for-byte the same source list as +# `psxrecomp-game` above — and was a leftover from before that target was +# introduced. Because Windows and macOS filesystems are case-insensitive, its +# output `PSXRecomp.exe` collided with the user-facing CLI `psxrecomp.exe` +# (the `psxrecomp` target above): both wrote the SAME file on disk and the +# last one linked won. In practice the CLI lost, so `psxrecomp.exe` shipped in +# `psxrecomp-cli-windows-x86_64.zip` (tools/build_cli.py) was actually the game +# recompiler, with a completely different argument grammar than the README +# documents. The bug is invisible on Linux, which is why it survived. +# The `PSXRecomp.exe` users are told to run in README's "Release Package" +# section is a DIFFERENT artifact — the BIOS-only runtime, whose name comes +# from OUTPUT_NAME in runtime/runtime.cmake — and is unaffected by this removal. +# See the collision guard at the bottom of this file. + +# psxrecomp-bios — Phase 1a BIOS boot slice recompiler. +# Strict, fail-loud, ingests bios/SCPH1001.BIN as a flat ROM at 0xBFC00000. +# Independent of PSXRecomp's PS1-EXE entry point. Shares mips_decoder + +# ps1_exe_parser sources because mips_decoder.h depends on PS1Executable +# (used only by decode_executable, which psxrecomp-bios does not call). +add_executable(psxrecomp-bios + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/strict_translator.cpp + src/pgxp_hook_emitter.cpp + src/bios_slice_walker.cpp + src/bios_address_model.cpp + src/function_discovery.cpp + src/full_function_emitter.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + src/main_bios.cpp +) +target_include_directories(psxrecomp-bios PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 +) +target_link_libraries(psxrecomp-bios PRIVATE fmt rabbitizer) +if(MINGW) + target_link_options(psxrecomp-bios PRIVATE -Wl,--stack,16777216) +elseif(MSVC) + target_link_options(psxrecomp-bios PRIVATE /STACK:16777216) +endif() + +# Released Windows CLI archives must run without a separate MinGW/MSYS2 +# install. Prefer llvm-mingw (+ -static) in CI so emitters do not need +# libgcc/libstdc++ harvested from MSYS2. Keep opt-in for local debug builds. +option(PSXRECOMP_STATIC_CLI "Statically link Windows runtime into CLI tools" OFF) +if(PSXRECOMP_STATIC_CLI AND WIN32 AND (MINGW OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + foreach(_cli_target psxrecomp psxrecomp-game psxrecomp-toml psxrecomp-bios) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_options(${_cli_target} PRIVATE + -static -static-libgcc -static-libstdc++) + else() + # llvm-mingw / Clang: fully static CRT + libc++ / libunwind / pthread. + target_link_options(${_cli_target} PRIVATE -static) + endif() + endforeach() +endif() + +# BIOS address-model equivalence test — sweeps every 4-byte address through +# the profile-driven BiosAddressModel and compares against verbatim copies of +# the pre-refactor hardcoded SCPH1001 helpers; also exercises the from_config +# invariants. Run from the framework root: recompiler/build/bios_address_model_test +add_executable(bios_address_model_test + tests/bios_address_model_test.cpp + src/bios_address_model.cpp + src/config_loader.cpp + src/ps1_exe_parser.cpp +) +target_include_directories(bios_address_model_test PRIVATE + include + src + lib/fmt/include + lib/toml11 +) +target_link_libraries(bios_address_model_test PRIVATE fmt) + +# L1 decoder conformance test — compares MipsDecoder against Rabbitizer +# oracle for every instruction in SCPH1001.BIN code regions. +add_executable(l1_decoder_test + tests/l1_decoder_test.cpp + src/mips_decoder.cpp + src/ps1_exe_parser.cpp +) +target_include_directories(l1_decoder_test PRIVATE + include + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include +) +target_link_libraries(l1_decoder_test PRIVATE fmt rabbitizer) + +# L2 structural codegen test — verifies StrictTranslator output patterns +# for each MIPS instruction form. +add_executable(l2_structural_test + tests/l2_structural_test.cpp + src/mips_decoder.cpp + src/ps1_exe_parser.cpp + src/strict_translator.cpp + src/pgxp_hook_emitter.cpp +) +target_include_directories(l2_structural_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include +) +target_link_libraries(l2_structural_test PRIVATE fmt rabbitizer) + +if(BUILD_TESTING) + add_executable(debug_trace_ranges_test + ../runtime/tests/test_debug_trace_ranges.c + ../runtime/src/debug_trace_ranges.c + ) + target_include_directories(debug_trace_ranges_test PRIVATE + ../runtime/include) + add_test(NAME debug_trace_ranges_test COMMAND debug_trace_ranges_test) + + add_executable(cli_boot_path_test + tests/cli_boot_path_test.cpp + src/cli_boot_path.cpp + ) + target_include_directories(cli_boot_path_test PRIVATE src) + add_test(NAME cli_boot_path_test COMMAND cli_boot_path_test) + + add_executable(recompiler_patch_test + tests/recompiler_patch_test.cpp + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/control_flow.cpp + src/function_analysis.cpp + src/code_generator.cpp + src/pgxp_hook_emitter.cpp + src/annotations.cpp + src/bios_address_model.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + ) + target_include_directories(recompiler_patch_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + lib/ELFIO + ) + target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer) + add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test) + + add_executable(full_function_emitter_test + tests/full_function_emitter_test.cpp + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/strict_translator.cpp + src/pgxp_hook_emitter.cpp + src/bios_address_model.cpp + src/function_discovery.cpp + src/full_function_emitter.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + ) + target_include_directories(full_function_emitter_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + ) + target_link_libraries(full_function_emitter_test PRIVATE fmt rabbitizer) + add_test(NAME full_function_emitter_test COMMAND full_function_emitter_test) + + add_executable(recompiler_patch_cxx17_header_test + tests/recompiler_patch_cxx17_header_test.cpp + ) + target_include_directories(recompiler_patch_cxx17_header_test PRIVATE src) + set_target_properties(recompiler_patch_cxx17_header_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) + add_test(NAME recompiler_patch_cxx17_header_test + COMMAND recompiler_patch_cxx17_header_test) + + # [video] geometry_correction / perspective_texturing opt-in plumbing. + add_executable(video_enhancement_settings_test + tests/video_enhancement_settings_test.cpp + src/config_loader.cpp + src/ps1_exe_parser.cpp + ) + target_include_directories(video_enhancement_settings_test PRIVATE + include + src + lib/fmt/include + lib/toml11 + ) + target_link_libraries(video_enhancement_settings_test PRIVATE fmt) + add_test(NAME video_enhancement_settings_test + COMMAND video_enhancement_settings_test) + + # Needs bios/SCPH1001.toml — run from the framework root. + add_test(NAME bios_address_model_test + COMMAND bios_address_model_test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + + # These targets were already built by every test build, but without + # add_test() entries ctest could neither run nor fail them. + add_test(NAME l2_structural_test COMMAND l2_structural_test) + add_test(NAME reachable_discovery_test COMMAND reachable_discovery_test) + + find_package(Python3 COMPONENTS Interpreter) + if(Python3_Interpreter_FOUND) + add_test( + NAME cli_project_packaging + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cli_project_packaging.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + + # These two invoke psxrecomp-game, whose BIOS-profile resolution + # probes fixed working-directory-relative paths (bios/.toml, + # then psxrecomp/bios/.toml) -- run them from the framework + # root so the shipped profiles are found. + add_test( + NAME overlay_cross_page_delay_codegen + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_cross_page_delay_codegen.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + add_test( + NAME fmv_cycle_batch_codegen + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fmv_cycle_batch_codegen.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + add_test( + NAME overlay_shim_compile_contract + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_shim_compile.py) add_test( NAME dirty_text_continuation_guards COMMAND ${Python3_EXECUTABLE} @@ -419,140 +424,140 @@ if(BUILD_TESTING) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) # ---- codegen tests that were written but never registered ----------- - # These existed in recompiler/tests/ with no add_test() entry, so they - # had no way to run and no way to fail. Each was executed manually - # before being wired here; all pass. They drive psxrecomp-game, so they - # run from the framework root for BIOS-profile resolution, exactly like - # the two above. - foreach(_t IN ITEMS - aot_base_recovery - bios_resident_shard - msvc_cps_codegen - overlay_guard_codegen - overlay_store_barrier_codegen - reachable_discovery_codegen) - add_test( - NAME ${_t} - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_${_t}.py - --recompiler $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - endforeach() - - # test_aot_overlay_discovery.py had no add_test() entry at all. It runs - # far enough to exercise real discovery, then fails in - # check_real_batched_fragment_publication: that check reaches - # MOD.compile_fragment_batch, which spawns psxrecomp-game with cwd set - # to its own TemporaryDirectory, so BIOS-profile resolution (which - # probes bios/SCPH1001.toml relative to the working directory) finds - # nothing and returns "recompiler-error: no BIOS profile found". Every - # other subprocess in the same file passes cwd=str(ROOT) explicitly, so - # this looks like a missed cwd rather than a product defect — but it - # needs an owner, and DISABLED keeps it counted until then. - add_test( - NAME aot_overlay_discovery - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_aot_overlay_discovery.py - --recompiler $ - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - - # ---- runtime source-invariant guards -------------------------------- - # These live in runtime/tests/ but only READ runtime source, so they - # need neither a generated BIOS nor a built runtime. Registering them - # here (as dirty_text_continuation_guards already was) means they run - # from a plain recompiler build, which is the only tree a newcomer can - # configure before regenerating a BIOS. Each was run manually first; - # all pass. - # - foreach(_t IN ITEMS - bios_selection_guards - capture_history - default_renderer_guards - mod_owned_display_controls - fmv_quiet_guards - fps_telemetry_opt_in - frame_interpolation_display_rate - hybrid_dev_any_input - launcher_vulkan_option - mod_controller_override + # These existed in recompiler/tests/ with no add_test() entry, so they + # had no way to run and no way to fail. Each was executed manually + # before being wired here; all pass. They drive psxrecomp-game, so they + # run from the framework root for BIOS-profile resolution, exactly like + # the two above. + foreach(_t IN ITEMS + aot_base_recovery + bios_resident_shard + msvc_cps_codegen + overlay_guard_codegen + overlay_store_barrier_codegen + reachable_discovery_codegen) + add_test( + NAME ${_t} + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_${_t}.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + endforeach() + + # test_aot_overlay_discovery.py had no add_test() entry at all. It runs + # far enough to exercise real discovery, then fails in + # check_real_batched_fragment_publication: that check reaches + # MOD.compile_fragment_batch, which spawns psxrecomp-game with cwd set + # to its own TemporaryDirectory, so BIOS-profile resolution (which + # probes bios/SCPH1001.toml relative to the working directory) finds + # nothing and returns "recompiler-error: no BIOS profile found". Every + # other subprocess in the same file passes cwd=str(ROOT) explicitly, so + # this looks like a missed cwd rather than a product defect — but it + # needs an owner, and DISABLED keeps it counted until then. + add_test( + NAME aot_overlay_discovery + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_aot_overlay_discovery.py + --recompiler $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + + # ---- runtime source-invariant guards -------------------------------- + # These live in runtime/tests/ but only READ runtime source, so they + # need neither a generated BIOS nor a built runtime. Registering them + # here (as dirty_text_continuation_guards already was) means they run + # from a plain recompiler build, which is the only tree a newcomer can + # configure before regenerating a BIOS. Each was run manually first; + # all pass. + # + foreach(_t IN ITEMS + bios_selection_guards + capture_history + default_renderer_guards + mod_owned_display_controls + fmv_quiet_guards + fps_telemetry_opt_in + frame_interpolation_display_rate + hybrid_dev_any_input + launcher_vulkan_option + mod_controller_override mod_load_acceleration overlay_candidate_capacity release_zip - overlay_decodable_fallback - overlay_init_guard - sdl3_main_single_include - vk_build_default - vk_color_self_barrier - vk_command_buffer_batching - vk_draw_area_batch_boundary - vk_mailbox_policy - vk_present_timing - vk_present_wait_stage - vk_staging_cache) - add_test( - NAME ${_t} - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - endforeach() - - # ---- known-failing guards: DISABLED, not de-registered --------------- - # These three were previously left out of the list above with a comment - # explaining that they FAIL and that wiring a red check would recreate - # the problem that took CI off PRs. That reasoning is right; the remedy - # was not. De-registering is precisely how a test stops being counted, - # and it is the mechanism that let 16 C/C++ tests rot in runtime/tests/. - # - # ctest's DISABLED property is the intended tool: the test stays - # registered, stays visible in `ctest -N`, and is reported as Disabled - # rather than silently absent — without turning the run red. Delete the - # matching set_tests_properties line to re-enable one once it is fixed. - # - # interpreter_perf_guards psx_devices_mmio_sync's recompute-deadline - # branch never clears g_psx_cycle_fast_limit, - # and the guard still names the pre-rename - # s_next_service_cycle - # runtime_perf_diag_guards asserts a substring no longer present in - # the runtime source - # overlay_pair_dedup_runtime needs its companion harness built; not a - # source-only guard - foreach(_t IN ITEMS - interpreter_perf_guards - runtime_perf_diag_guards - overlay_pair_dedup_runtime) - add_test( - NAME ${_t} - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) - set_tests_properties(${_t} PROPERTIES DISABLED TRUE) - endforeach() - endif() -endif() - -# Reachable main-EXE discovery test: parser-bound validation plus exact-entry -# analysis behavior for direct, indirect, seeded, and out-of-bound targets. -add_executable(reachable_discovery_test - tests/reachable_discovery_test.cpp - src/function_analysis.cpp - src/ps1_exe_parser.cpp -) -target_include_directories(reachable_discovery_test PRIVATE - include - lib/fmt/include -) -target_link_libraries(reachable_discovery_test PRIVATE fmt) - -# ---- test-registration completeness guard ----------------------------------- -# Reads both CMakeLists.txt files as text, so it catches an orphan in either -# project from whichever one is being configured. Placed before the collision -# guard, which must stay last because it needs every target declared; this one -# inspects files on disk and has no such ordering requirement. -include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/check_test_registration.cmake) -psxrecomp_check_all_tests_registered() - -# ---- output-name collision guard (must stay LAST) --------------------------- -# Fails configure if any two targets here would write the same file on a -# case-insensitive filesystem. See the module header for the shipped-bug story. -include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/check_target_name_collisions.cmake) -psxrecomp_check_output_name_collisions() + overlay_decodable_fallback + overlay_init_guard + sdl3_main_single_include + vk_build_default + vk_color_self_barrier + vk_command_buffer_batching + vk_draw_area_batch_boundary + vk_mailbox_policy + vk_present_timing + vk_present_wait_stage + vk_staging_cache) + add_test( + NAME ${_t} + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + endforeach() + + # ---- known-failing guards: DISABLED, not de-registered --------------- + # These three were previously left out of the list above with a comment + # explaining that they FAIL and that wiring a red check would recreate + # the problem that took CI off PRs. That reasoning is right; the remedy + # was not. De-registering is precisely how a test stops being counted, + # and it is the mechanism that let 16 C/C++ tests rot in runtime/tests/. + # + # ctest's DISABLED property is the intended tool: the test stays + # registered, stays visible in `ctest -N`, and is reported as Disabled + # rather than silently absent — without turning the run red. Delete the + # matching set_tests_properties line to re-enable one once it is fixed. + # + # interpreter_perf_guards psx_devices_mmio_sync's recompute-deadline + # branch never clears g_psx_cycle_fast_limit, + # and the guard still names the pre-rename + # s_next_service_cycle + # runtime_perf_diag_guards asserts a substring no longer present in + # the runtime source + # overlay_pair_dedup_runtime needs its companion harness built; not a + # source-only guard + foreach(_t IN ITEMS + interpreter_perf_guards + runtime_perf_diag_guards + overlay_pair_dedup_runtime) + add_test( + NAME ${_t} + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_${_t}.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + set_tests_properties(${_t} PROPERTIES DISABLED TRUE) + endforeach() + endif() +endif() + +# Reachable main-EXE discovery test: parser-bound validation plus exact-entry +# analysis behavior for direct, indirect, seeded, and out-of-bound targets. +add_executable(reachable_discovery_test + tests/reachable_discovery_test.cpp + src/function_analysis.cpp + src/ps1_exe_parser.cpp +) +target_include_directories(reachable_discovery_test PRIVATE + include + lib/fmt/include +) +target_link_libraries(reachable_discovery_test PRIVATE fmt) + +# ---- test-registration completeness guard ----------------------------------- +# Reads both CMakeLists.txt files as text, so it catches an orphan in either +# project from whichever one is being configured. Placed before the collision +# guard, which must stay last because it needs every target declared; this one +# inspects files on disk and has no such ordering requirement. +include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/check_test_registration.cmake) +psxrecomp_check_all_tests_registered() + +# ---- output-name collision guard (must stay LAST) --------------------------- +# Fails configure if any two targets here would write the same file on a +# case-insensitive filesystem. See the module header for the shipped-bug story. +include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/check_target_name_collisions.cmake) +psxrecomp_check_output_name_collisions() diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 3d882e8b..7ddae12f 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -1,4 +1,5 @@ #include "code_generator.h" +#include "pgxp_hook_emitter.h" #include "control_flow.h" #include "gte_register_classification.h" #include "../src/bios_address_model.h" @@ -1572,12 +1573,19 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); bool special = PSXRecompGTERegisters::data_write_needs_helper(static_cast(rt)); + /* The raw loaded word is captured for the PGXP hook: the + * register may hold a MASKED value (write helper), and + * the shadow must validate against the word as loaded. */ if (special) { - code = gte_stall + fmt::format("gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {})); /* lwc2 gte[{}] */", - rt, addr, rt); + code = gte_stall + fmt::format( + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); " + "gte_write_data(cpu, {}, _pgxv); PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* lwc2 gte[{}] */", + addr, rt, instr, rt); } else { - code = gte_stall + fmt::format("cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {}); /* lwc2 gte[{}], ({}) */", - rt, addr, rt, addr); + code = gte_stall + fmt::format( + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); " + "cpu->gte_data[{}] = _pgxv; PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* lwc2 gte[{}], ({}) */", + addr, rt, instr, rt, addr); } } break; @@ -1593,16 +1601,15 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) ? fmt::format("gte_read_data(cpu, {})", rt) : fmt::format("cpu->gte_data[{}]", rt); std::string swc2_store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", addr); - if (offset == 0) { - code = gte_stall + swc2_store_pc + fmt::format( - "psx_store_cycle_barrier(); cpu->write_word({}, {}); gte_precision_store_word({}, {}); /* swc2 gte[{}], ({}) */", - reg_name(rs), value, reg_name(rs), rt, rt, reg_name(rs)); - } else { - code = gte_stall + swc2_store_pc + fmt::format( - "psx_store_cycle_barrier(); cpu->write_word({} + {}, {}); gte_precision_store_word({} + {}, {}); /* swc2 gte[{}], {}({}) */", - reg_name(rs), offset, value, reg_name(rs), offset, rt, - rt, offset, reg_name(rs)); - } + std::string swc2_addr = (offset == 0) + ? reg_name(rs) + : fmt::format("{} + {}", reg_name(rs), offset); + code = gte_stall + swc2_store_pc + fmt::format( + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = {}; " + "psx_store_cycle_barrier(); cpu->write_word(_pgxa, _pgxv); " + "gte_precision_store_word(_pgxa, {}); " + "PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* swc2 gte[{}], {}({}) */", + swc2_addr, value, rt, instr, rt, offset, reg_name(rs)); } break; default: @@ -1610,6 +1617,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) } } + PSXRecomp::append_pgxp_hooks(instr, code); return config_.indent + code + comment; } diff --git a/recompiler/src/pgxp_hook_emitter.cpp b/recompiler/src/pgxp_hook_emitter.cpp new file mode 100644 index 00000000..1455983c --- /dev/null +++ b/recompiler/src/pgxp_hook_emitter.cpp @@ -0,0 +1,134 @@ +#include "pgxp_hook_emitter.h" + +#include + +namespace PSXRecomp { + +namespace { +/* Same register naming both emitters use ("cpu->gpr[n]"); kept local so this + * TU links into psxrecomp-bios without pulling the game CodeGenerator. */ +inline std::string reg_name(uint32_t n) { return fmt::format("cpu->gpr[{}]", n); } +inline uint32_t get_rs(uint32_t i) { return (i >> 21) & 0x1F; } +inline uint32_t get_rt(uint32_t i) { return (i >> 16) & 0x1F; } +inline uint32_t get_rd(uint32_t i) { return (i >> 11) & 0x1F; } +inline int16_t get_imm16(uint32_t i) { return (int16_t)(i & 0xFFFF); } +} + +/* PGXP dataflow-shadowing hook emission (ENHANCEMENTS.md G1.10; pgxp_hooks.h). + * + * Wraps the translated statement with a PGXP_*() macro carrying the raw + * instruction word plus the operand values the runtime engine needs. Source + * operands that the statement may clobber (a load/ALU destination aliasing a + * source, a load overwriting its own base register) are captured into locals + * BEFORE the statement; everything else is read after it. In the base build + * the macros preprocess to ((void)0) and the optimizer erases the dead + * capture locals, so base objects are unchanged; only a -DPSX_PGXP=1 TU pays. + * + * Deliberately unhooked: AND/XOR/NOR/SLT-family and the exotic immediates — + * they only ever DESTROY precision, and the engine's validate-on-read drops + * their stale shadows without help. The configured widescreen special sites + * return early above translate_instruction's main dispatch and are likewise + * unhooked (they are cull compares, not vertex moves; validation covers). */ +void append_pgxp_hooks(uint32_t instr, std::string& code) { + const uint32_t opcode = (instr >> 26) & 0x3F; + const uint32_t rs = get_rs(instr); + const uint32_t rt = get_rt(instr); + const uint32_t rd = get_rd(instr); + const int16_t offset = get_imm16(instr); + const auto addr_expr = [&]() { + return (offset == 0) ? reg_name(rs) + : fmt::format("{} + {}", reg_name(rs), offset); + }; + + switch (opcode) { + case 0x00: { /* SPECIAL */ + const uint32_t funct = instr & 0x3F; + switch (funct) { + case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */ + if (rd == 0 || instr == 0) return; /* skip plain nop */ + code = fmt::format( + "{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, {}u); }}", + reg_name(rt), code, instr, reg_name(rd), (instr >> 6) & 31u); + return; + case 0x04: case 0x06: case 0x07: /* SLLV / SRLV / SRAV */ + if (rd == 0) return; + code = fmt::format( + "{{ uint32_t _pgx1 = {}; uint32_t _pgx2 = {}; {} " + "PGXP_ALU(0x{:08X}u, {}, _pgx1, _pgx2); }}", + reg_name(rt), reg_name(rs), code, instr, reg_name(rd)); + return; + case 0x10: case 0x12: /* MFHI / MFLO */ + if (rd == 0) return; + code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, {}, 0u);", + code, instr, reg_name(rd), + funct == 0x10 ? "cpu->hi" : "cpu->lo"); + return; + case 0x11: case 0x13: /* MTHI / MTLO */ + code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, {}, 0u);", + code, instr, + funct == 0x11 ? "cpu->hi" : "cpu->lo", + funct == 0x11 ? "cpu->hi" : "cpu->lo"); + return; + case 0x18: case 0x19: case 0x1A: case 0x1B: /* MULT(U)/DIV(U) */ + code = fmt::format( + "{} PGXP_MULDIV(0x{:08X}u, cpu->hi, cpu->lo, {}, {});", + code, instr, reg_name(rs), reg_name(rt)); + return; + case 0x20: case 0x21: case 0x22: case 0x23: /* ADD(U)/SUB(U) */ + case 0x25: /* OR */ + if (rd == 0) return; + code = fmt::format( + "{{ uint32_t _pgx1 = {}; uint32_t _pgx2 = {}; {} " + "PGXP_ALU(0x{:08X}u, {}, _pgx1, _pgx2); }}", + reg_name(rs), reg_name(rt), code, instr, reg_name(rd)); + return; + default: + return; + } + } + case 0x08: case 0x09: /* ADDI / ADDIU */ + if (rt == 0) return; + code = fmt::format( + "{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, 0x{:08X}u); }}", + reg_name(rs), code, instr, reg_name(rt), + (uint32_t)(int32_t)offset); + return; + case 0x0D: /* ORI */ + if (rt == 0) return; + code = fmt::format( + "{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, 0x{:04X}u); }}", + reg_name(rs), code, instr, reg_name(rt), + (uint32_t)(uint16_t)offset); + return; + case 0x0F: /* LUI */ + if (rt == 0) return; + code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, 0u, 0u);", + code, instr, reg_name(rt)); + return; + case 0x12: { /* COP2 register transfers */ + const uint32_t cop_op = (instr >> 21) & 0x1F; + if ((cop_op == 0x00 && rt != 0) || cop_op == 0x04) /* MFC2 / MTC2 */ + code = fmt::format("{} PGXP_COP2(0x{:08X}u, {}, 0u);", + code, instr, reg_name(rt)); + return; + } + case 0x20: case 0x21: case 0x22: case 0x23: + case 0x24: case 0x25: case 0x26: /* loads */ + if (rt == 0) return; + code = fmt::format( + "{{ uint32_t _pgxa = {}; {} PGXP_LOAD(0x{:08X}u, _pgxa, {}); }}", + addr_expr(), code, instr, reg_name(rt)); + return; + case 0x28: case 0x29: case 0x2A: case 0x2B: + case 0x2E: /* stores */ + code = fmt::format( + "{{ uint32_t _pgxa = {}; {} PGXP_STORE(0x{:08X}u, _pgxa, {}); }}", + addr_expr(), code, instr, reg_name(rt)); + return; + default: + return; + } +} + + +} // namespace PSXRecomp diff --git a/recompiler/src/pgxp_hook_emitter.h b/recompiler/src/pgxp_hook_emitter.h new file mode 100644 index 00000000..6f3b8dab --- /dev/null +++ b/recompiler/src/pgxp_hook_emitter.h @@ -0,0 +1,31 @@ +#ifndef PGXP_HOOK_EMITTER_H +#define PGXP_HOOK_EMITTER_H + +#include +#include + +namespace PSXRecomp { + +/* PGXP dataflow-shadowing hook emission (ENHANCEMENTS.md G1.10; runtime + * pgxp_hooks.h). Shared by BOTH emitters — CodeGenerator (game) and + * StrictTranslator (BIOS) — so the hook grammar can never drift between them. + * + * Wraps the translated statement `code` with a PGXP_*() macro invocation for + * the instruction classes that move sub-pixel projection provenance. Source + * operands the statement may clobber are captured into locals BEFORE it; + * everything else is read after it. In the base build the macros preprocess + * to ((void)0) and the optimizer erases the dead captures, so base objects + * are unchanged; only a -DPSX_PGXP=1 TU pays. + * + * NOT applied to LWC2/SWC2 — those emissions capture the raw loaded/stored + * word themselves (a masked GTE register write must validate against the + * word as loaded, not the register's masked value). + * + * Deliberately unhooked: AND/XOR/NOR/SLT-family and the exotic immediates — + * they only ever DESTROY precision, and the engine's validate-on-read drops + * their stale shadows without help. */ +void append_pgxp_hooks(uint32_t instr, std::string& code); + +} // namespace PSXRecomp + +#endif /* PGXP_HOOK_EMITTER_H */ diff --git a/recompiler/src/strict_translator.cpp b/recompiler/src/strict_translator.cpp index e72eb9e5..85be8666 100644 --- a/recompiler/src/strict_translator.cpp +++ b/recompiler/src/strict_translator.cpp @@ -7,6 +7,7 @@ // records this substitution explicitly. #include "strict_translator.h" +#include "pgxp_hook_emitter.h" /* shared PGXP hook grammar */ #include "gte_register_classification.h" #include @@ -63,6 +64,30 @@ TranslateResult unsupported(const PSXRecomp::DecodedInstruction& d, const std::s } // namespace TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& d) { + TranslateResult r = translate_impl(d); + if (!r.supported) return r; + const uint32_t opcode = (d.raw >> 26) & 0x3F; + /* Shared PGXP hook grammar (ENHANCEMENTS.md G1.10). LWC2/SWC2 capture + * their raw word inside translate_impl and are skipped here. */ + if (opcode != 0x32 && opcode != 0x3A && !r.c_code.empty()) + PSXRecomp::append_pgxp_hooks(d.raw, r.c_code); + /* The deferred load-delay variant assigns into the function-scope + * psx_ldd_ temp instead of the GPR; hook it with that temp as the + * loaded value (the shadow keys on the value, not on where it lands — + * the emitter's later writeback makes the GPR match). */ + if (r.load_dest >= 0 && !r.c_code_deferred.empty()) { + const uint32_t rs = (d.raw >> 21) & 0x1F; + const int16_t offset = static_cast(d.raw & 0xFFFF); + r.c_code_deferred = fmt::format( + "{{ uint32_t _pgxa = cpu->gpr[{}] + {}; {} " + "PGXP_LOAD(0x{:08X}u, _pgxa, psx_ldd_{:08X}); }}", + static_cast(rs), static_cast(offset), + r.c_code_deferred, d.raw, d.address); + } + return r; +} + +TranslateResult StrictTranslator::translate_impl(const PSXRecomp::DecodedInstruction& d) { TranslateResult r; // NOP (raw == 0, i.e. SLL $zero,$zero,0) — emit as a comment with no semantics. @@ -1405,14 +1430,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& ? fmt::format("cpu->gpr[{}]", static_cast(rs)) : fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast(rs), static_cast(offset)); bool special = PSXRecompGTERegisters::data_write_needs_helper(rt); + /* The raw loaded word is captured for the PGXP hook: a masked + * register write must validate against the word as loaded. */ if (special) { r.c_code = gte_stall + fmt::format( - "gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {}));", - static_cast(rt), addr); + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); " + "gte_write_data(cpu, {}, _pgxv); PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}", + addr, static_cast(rt), d.raw); } else { r.c_code = gte_stall + fmt::format( - "cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {});", - static_cast(rt), addr); + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); " + "cpu->gte_data[{}] = _pgxv; PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}", + addr, static_cast(rt), d.raw); } r.comment = fmt::format("lwc2 gte[{}], {}({})", rt, static_cast(offset), gpr_name(rs)); @@ -1431,20 +1460,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& std::string value = PSXRecompGTERegisters::data_read_needs_helper(rt) ? fmt::format("gte_read_data(cpu, {})", static_cast(rt)) : fmt::format("cpu->gte_data[{}]", static_cast(rt)); - if (offset == 0) { + { + std::string addr = offset == 0 + ? fmt::format("cpu->gpr[{}]", static_cast(rs)) + : fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", + static_cast(rs), static_cast(offset)); r.c_code = gte_stall + fmt::format( "psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; " - "cpu->write_word(cpu->gpr[{}], {}); " - "gte_precision_store_word(cpu->gpr[{}], {});", - d.address, static_cast(rs), value, - static_cast(rs), static_cast(rt)); - } else { - r.c_code = gte_stall + fmt::format( - "psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; " - "cpu->write_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {}); " - "gte_precision_store_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {});", - d.address, static_cast(rs), static_cast(offset), value, - static_cast(rs), static_cast(offset), static_cast(rt)); + "{{ uint32_t _pgxa = {}; uint32_t _pgxv = {}; " + "cpu->write_word(_pgxa, _pgxv); " + "gte_precision_store_word(_pgxa, {}); " + "PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}", + d.address, addr, value, static_cast(rt), d.raw); } r.comment = fmt::format("swc2 gte[{}], {}({})", rt, static_cast(offset), gpr_name(rs)); diff --git a/recompiler/src/strict_translator.h b/recompiler/src/strict_translator.h index 777defba..c13ca58d 100644 --- a/recompiler/src/strict_translator.h +++ b/recompiler/src/strict_translator.h @@ -63,6 +63,13 @@ class StrictTranslator { public: // Translate a single decoded instruction. Pure function — no state. static TranslateResult translate(const PSXRecomp::DecodedInstruction& d); + +private: + // The per-opcode translation body; translate() wraps its c_code with the + // shared PGXP hook grammar (CodeGenerator::append_pgxp_hooks). + static TranslateResult translate_impl(const PSXRecomp::DecodedInstruction& d); + +public: }; } // namespace PSXRecompV4 diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index f71dff85..7b4363ff 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -17,6 +17,23 @@ psxrecomp_add_runtime_target(psx-runtime EXE_NAME "PSXRecomp" ) +# PGXP variant of the framework runtime (ENHANCEMENTS.md G1.10): the same +# recompiled BIOS compiled with -DPSX_PGXP=1 so the emitted PGXP_*() hook +# macros arm the value-propagation engine. Framework-dev validation surface +# (LLE boot logo is real GTE content); titles get theirs via +# PSX_PGXP_VARIANT in psxrecomp_add_game_runtime. +option(PSX_PGXP_VARIANT + "Also build the _pgxp PGXP precision-shadowing variant" OFF) +if(PSX_PGXP_VARIANT) + psxrecomp_add_runtime_target(psx-runtime-pgxp + PGXP + DEBUG_PORT 4370 + WINDOW_TITLE "psxrecomp - BIOS" + DEFAULT_BIOS_PATH "${PSXRECOMP_ROOT}/bios/SCPH1001.BIN" + EXE_NAME "PSXRecomp" + ) +endif() + if(BUILD_TESTING) add_executable(launcher_device_roundtrip_test tests/test_launcher_device_roundtrip.cpp) diff --git a/runtime/codegen_hash_sources.cmake b/runtime/codegen_hash_sources.cmake index 3651f39e..96124d2b 100644 --- a/runtime/codegen_hash_sources.cmake +++ b/runtime/codegen_hash_sources.cmake @@ -28,6 +28,8 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.h + ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/pgxp_hook_emitter.cpp + ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/pgxp_hook_emitter.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_discovery.cpp ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_discovery.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/include/gte_register_classification.h diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index ae4f8602..2ceda151 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -86,6 +86,13 @@ * model participation with queue-safe guard/hysteresis policy. */ /* v21: ws_angle_widen forwarder for exact, aspect-scaled 12-bit terrain * frustum half-angle constants. */ +/* (PGXP, no version bump): OverlayCallbacks grew an appended-last PGXPHooks + * table pointer (pgxp_hooks.h). Only pgxp-flavour shards (compiled with + * -DPSX_PGXP=1, PSX_OVERLAY_FLAVOR_PGXP set) reference the psx_pgxp_* + * forwarders, and the flavor half of the ABI tag already rejects any + * host/DLL flavor mix — base-flavour DLLs and hosts are untouched, so + * the version stays. The emit-content change (PGXP_*() macros in all + * generated C) is covered by the codegen hash + CODEGEN_VER below. */ #define PSX_OVERLAY_ABI_VERSION 21 /* Process-lifetime overlay candidate capacity. Every accepted manifest F @@ -113,6 +120,12 @@ #define PSX_OVERLAY_FLAVOR 0 #endif +/* Flavor bits (compose bitwise; base = 0): + * 1 — widescreen variant codegen + * 2 — PGXP precision shadowing (-DPSX_PGXP=1 objects; pgxp_hooks.h) */ +#define PSX_OVERLAY_FLAVOR_WIDESCREEN 1 +#define PSX_OVERLAY_FLAVOR_PGXP 2 + /* Combined tag exported by overlay_abi() and checked by the loader. */ #define PSX_OVERLAY_ABI_TAG \ ((int)((PSX_OVERLAY_ABI_VERSION & 0xFFFF) | ((PSX_OVERLAY_FLAVOR & 0xFFFF) << 16))) @@ -139,8 +152,11 @@ * masked-16-bit screen-X, and depth cull emits. * 7: + configured side-plane normal-X (plane_nx) cull emit. * 8: + configured per-primitive bound-load (xclip) cull emit. - * 9: + full-word-guarded object/model participation compare emit. */ -#define PSX_OVERLAY_CODEGEN_VER 9 + * 9: + full-word-guarded object/model participation compare emit. + * 10: + PGXP dataflow-shadowing PGXP_*() macro emission at loads/stores/ + * COP2 transfers/precision-carrying ALU ops (pgxp_hooks.h; no-ops in + * base-flavour objects, real hooks under -DPSX_PGXP=1). */ +#define PSX_OVERLAY_CODEGEN_VER 10 /* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen * sources). Folded into the cache PATH next to cg (gcc//cg_/) diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc index 6cd4b10e..c2acb8e7 100644 --- a/runtime/include/overlay_dispatch_preamble.c.inc +++ b/runtime/include/overlay_dispatch_preamble.c.inc @@ -265,4 +265,27 @@ uint32_t psx_ws_angle_widen(uint32_t vanilla) { void gte_precision_store_word(uint32_t addr, uint8_t reg) { if (g_cbs.gte_precision_store_word) g_cbs.gte_precision_store_word(addr, reg); } +#if defined(PSX_PGXP) && PSX_PGXP +/* PGXP dataflow-shadowing hooks (pgxp_hooks.h; ENHANCEMENTS.md G1.10). Only a + * pgxp-flavour shard (compiled -DPSX_PGXP=1, flavor bit set) references these + * — base-flavour shards preprocess the PGXP_*() macros away, so this block is + * compiled out of them entirely. Forwards to the host engine through the + * appended-last table; every hook no-ops on a host that predates it + * (precision shadowing is visual-only, never load-bearing). */ +void psx_pgxp_load(struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value) { + if (g_cbs.pgxp && g_cbs.pgxp->load) g_cbs.pgxp->load(cpu, instr, addr, value); +} +void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value) { + if (g_cbs.pgxp && g_cbs.pgxp->store) g_cbs.pgxp->store(cpu, instr, addr, value); +} +void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2) { + if (g_cbs.pgxp && g_cbs.pgxp->alu) g_cbs.pgxp->alu(cpu, instr, result, s1, s2); +} +void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2) { + if (g_cbs.pgxp && g_cbs.pgxp->muldiv) g_cbs.pgxp->muldiv(cpu, instr, hi, lo, s1, s2); +} +void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr) { + if (g_cbs.pgxp && g_cbs.pgxp->cop2) g_cbs.pgxp->cop2(cpu, instr, value, addr); +} +#endif /* PSX_PGXP */ /* ----------------------------------------------------------------------- */ diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index d8bc2e7e..c6ba06d0 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -92,6 +92,8 @@ typedef struct PGXPStats { uint64_t trunc_reject; /* integer part disagreed with native parse */ uint64_t tolerance_reject; uint64_t w_valid; /* lookups that also carried a usable depth */ + uint64_t produced; /* RTPS/RTPT projections pushed into shadows */ + uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */ } PGXPStats; void pgxp_get_stats(PGXPStats *out); @@ -115,6 +117,8 @@ void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed, int32_t *x16, int32_t *y16, uint16_t *z, uint8_t *valid); uint32_t pgxp_test_generation(void); +uint32_t pgxp_test_suppress_depth(void); +int pgxp_test_active(void); void pgxp_test_set_generation(uint32_t gen); #ifdef __cplusplus diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 2c9ae0e4..08c2b31e 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -761,7 +761,13 @@ function(psxrecomp_ensure_zlib) endfunction() function(psxrecomp_add_runtime_target target) - set(options ORACLE COSIM) + # PGXP: build this target's objects with -DPSX_PGXP=1 so the PGXP_*() + # hook macros the emitter writes into ALL generated C become real calls + # into the value-propagation engine (pgxp_hooks.h; ENHANCEMENTS.md G1.10), + # and stamp the pgxp overlay flavor so the shard cache and the ABI gate + # keep pgxp and base DLLs fully separate. The base target is untouched — + # the macros preprocess away without the define. + set(options ORACLE COSIM PGXP) set(oneValueArgs GAME_GENERATED_DISPATCH_C GAME_OVERLAY_STATIC_C @@ -917,6 +923,15 @@ function(psxrecomp_add_runtime_target target) if(PSXRT_ORACLE) set(_psxrt_exe_name "${_psxrt_exe_name}_oracle") endif() + if(PSXRT_PGXP) + # Distinct binary beside the base one; the launcher (or the player) + # picks the variant. Same debug port as the base build — run one at a + # time (the A/B protocol is one-toggle-per-run anyway). + set(_psxrt_exe_name "${_psxrt_exe_name}_pgxp") + target_compile_definitions(${target} PRIVATE + PSX_PGXP=1 + PSX_OVERLAY_FLAVOR=2) # PSX_OVERLAY_FLAVOR_PGXP (overlay_api.h) + endif() set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}") # ---- Windows / desktop app icon --------------------------------------- @@ -1790,20 +1805,41 @@ function(psxrecomp_add_game_runtime target) GAME_GENERATED_DISPATCH_C "${_psxg_marker}" ${_psxg_rt_args} ) + set(_psxg_targets ${target}) + # PGXP variant (ENHANCEMENTS.md G1.10): the SAME generated sources + # compiled a second time with -DPSX_PGXP=1 into _pgxp.exe. The + # emitted PGXP_*() macros become real hook calls, arming true + # geometry correction; the base target is byte-for-byte the + # pre-feature build. Opt-in per configure — it roughly doubles the + # generated-C compile time. + option(PSX_PGXP_VARIANT + "Also build the _pgxp PGXP precision-shadowing variant" OFF) + if(PSX_PGXP_VARIANT) + psxrecomp_add_runtime_target(${target}-pgxp + PGXP + GAME_GENERATED_FULL_C ${_psxg_full_list} + GAME_GENERATED_DISPATCH_C "${_psxg_marker}" + ${_psxg_rt_args} + ) + list(APPEND _psxg_targets ${target}-pgxp) + endif() else() message(STATUS "psxrecomp: setup host (no game C, no BIOS backends) — " "first-run Generate & rebuild") psxrecomp_add_runtime_target(${target} ${_psxg_rt_args}) + set(_psxg_targets ${target}) endif() - target_compile_definitions(${target} PRIVATE PSX_HAS_GAME_CODEGEN=1) + foreach(_psxg_t IN LISTS _psxg_targets) + target_compile_definitions(${_psxg_t} PRIVATE PSX_HAS_GAME_CODEGEN=1) - if(PSX_NET_LOBBY_DEFAULT_URL) - # Stringify for C: PSX_NET_LOBBY_DEFAULT_URL="ws://..." - target_compile_definitions(${target} PRIVATE - "PSX_NET_LOBBY_DEFAULT_URL=\"${PSX_NET_LOBBY_DEFAULT_URL}\"") - endif() + if(PSX_NET_LOBBY_DEFAULT_URL) + # Stringify for C: PSX_NET_LOBBY_DEFAULT_URL="ws://..." + target_compile_definitions(${_psxg_t} PRIVATE + "PSX_NET_LOBBY_DEFAULT_URL=\"${PSX_NET_LOBBY_DEFAULT_URL}\"") + endif() + endforeach() # Include the portable codegen host. Do NOT add CMAKE_CURRENT_SOURCE_DIR # wholesale to -I: on case-insensitive macOS, #include can pick @@ -1816,5 +1852,7 @@ function(psxrecomp_add_game_runtime target) if(RECOMP_UI_ROOT) list(APPEND _psxg_inc "${RECOMP_UI_ROOT}/src") endif() - target_include_directories(${target} PRIVATE ${_psxg_inc}) + foreach(_psxg_t IN LISTS _psxg_targets) + target_include_directories(${_psxg_t} PRIVATE ${_psxg_inc}) + endforeach() endfunction() diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 566e42d4..7ec45f10 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5212,7 +5212,8 @@ static void handle_geom_correction(int id, const char *json) "\"pgxp\":{\"enabled\":%d,\"cpu_mode\":%d,\"tolerance\":%.3f," "\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu," "\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu," - "\"tolerance_reject\":%llu,\"w_valid\":%llu}}", + "\"tolerance_reject\":%llu,\"w_valid\":%llu," + "\"produced\":%llu,\"swc2_stores\":%llu}}", id, gte_geometry_correction_enabled(), (unsigned)hits, @@ -5226,7 +5227,9 @@ static void handle_geom_correction(int id, const char *json) (unsigned long long)ps.value_mismatch, (unsigned long long)ps.trunc_reject, (unsigned long long)ps.tolerance_reject, - (unsigned long long)ps.w_valid); + (unsigned long long)ps.w_valid, + (unsigned long long)ps.produced, + (unsigned long long)ps.swc2_stores); } /* pgxp — live-tune the value-propagation engine for one-toggle isolation runs @@ -5260,8 +5263,9 @@ static void handle_pgxp(int id, const char *json) pgxp_set_tolerance((float)strtod(p, NULL)); } send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d," - "\"tolerance\":%.3f}", - id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance()); + "\"tolerance\":%.3f,\"suppress\":%u,\"active\":%d}", + id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(), + (unsigned)pgxp_test_suppress_depth(), pgxp_test_active()); } static void handle_gpu_state(int id, const char *json) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index a0b0c554..0ad1435a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -12123,6 +12123,16 @@ session_reboot: /* Sub-pixel vertex precision + perspective-correct UVs. Both default off; * with both off every setter below leaves the tracking caches disabled and * the draw path is the faithful integer one, unchanged. */ + /* Env overrides (debug/validation path, like PSX_BIOS_HLE): arm the + * corrections from process start so free-running (headless) boots can be + * measured from the first projected vertex — a TCP toggle always arrives + * after the interesting window. '0' = off, anything else = on. */ + if (const char* e = std::getenv("PSX_GEOMETRY_CORRECTION")) + g_video_geometry_correction = (*e && *e != '0') ? 1 : 0; + if (const char* e = std::getenv("PSX_PERSPECTIVE_TEXTURING")) + g_video_perspective_texturing = (*e && *e != '0') ? 1 : 0; + if (const char* e = std::getenv("PSX_PGXP_CPU_MODE")) + g_video_pgxp_cpu_mode = (*e && *e != '0') ? 1 : 0; gte_geometry_correction_set(g_video_geometry_correction); gpu_texture_correction_set(g_video_perspective_texturing); pgxp_set_cpu_mode(g_video_pgxp_cpu_mode); diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index edd5af69..6a0ca686 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -1727,9 +1727,9 @@ static void warn_on_cgtag_mismatch(const char *tier) { snprintf(base, sizeof base, "%s/%s/%s/%s", s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI); char expect[64]; - snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x", + snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x_f%u", PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH, - (unsigned)s_config_hash); + (unsigned)s_config_hash, (unsigned)PSX_OVERLAY_FLAVOR); snprintf(pattern, sizeof pattern, "%s/cg*", base); WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(pattern, &fd); @@ -1755,9 +1755,9 @@ static void warn_on_cgtag_mismatch(const char *tier) { char base[768], expect[64], found[256]; snprintf(base, sizeof base, "%s/%s/%s/%s", s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI); - snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x", + snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x_f%u", PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH, - (unsigned)s_config_hash); + (unsigned)s_config_hash, (unsigned)PSX_OVERLAY_FLAVOR); if (psx_overlay_posix_find_other_cache_tag(base, expect, found, sizeof(found))) { loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but " @@ -1906,14 +1906,16 @@ static void scan_cache_dir(void) { /* Index both tiers and every immutable artifact. Runtime selection prefers * usable GCC over TCC; an invalid GCC artifact cannot suppress a valid TCC * fallback merely because its filename was enumerated first. */ - snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x_gc%08x", + snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x_gc%08x_f%u", s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER, - (unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash); + (unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash, + (unsigned)PSX_OVERLAY_FLAVOR); scan_one_cache_dir(dir, CACHE_TIER_GCC); abi_preflight_sweep(dir); - snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x_gc%08x", + snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x_gc%08x_f%u", s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER, - (unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash); + (unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash, + (unsigned)PSX_OVERLAY_FLAVOR); scan_one_cache_dir(dir, CACHE_TIER_TCC); abi_preflight_sweep(dir); @@ -2355,6 +2357,21 @@ static void init_callbacks(void) { extern uint32_t psx_ws_angle_widen(uint32_t vanilla); s_callbacks.ws_angle_widen = psx_ws_angle_widen; } + /* PGXP dataflow-shadowing hook table (pgxp_hooks.h, appended last). + * Referenced only by pgxp-flavour shards; the flavor half of the ABI + * tag already rejects any host/DLL flavor mix, and a NULL table on an + * older host makes every hook a no-op (visual-only, never + * load-bearing). */ + { + static const PGXPHooks pgxp_hooks_table = { + psx_pgxp_load, + psx_pgxp_store, + psx_pgxp_alu, + psx_pgxp_muldiv, + psx_pgxp_cop2, + }; + s_callbacks.pgxp = &pgxp_hooks_table; + } } } diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 87f19332..c8faaa6d 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -631,6 +631,7 @@ extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed) { if (!g_pgxp_active) return; + s_stats.produced++; s_gte[12] = s_gte[13]; s_gte[13] = s_gte[14]; PGXPValue *pv = &s_gte[14]; @@ -735,6 +736,7 @@ extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) { if (!dst) return; const PGXPValue *src = &s_gte[reg & 31u]; if (src->gen != s_gen) return; + s_stats.swc2_stores++; *dst = *src; } @@ -769,6 +771,9 @@ extern "C" void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed, extern "C" uint32_t pgxp_test_generation(void) { return s_gen; } +extern "C" uint32_t pgxp_test_suppress_depth(void) { return s_suppress; } +extern "C" int pgxp_test_active(void) { return g_pgxp_active; } + extern "C" void pgxp_test_set_generation(uint32_t gen) { s_gen = gen; } /* Address-keyed depth lookup for the shipped perspective-texturing path diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index 14cc265d..b3f7a3c0 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -4068,6 +4068,9 @@ def _compile_dll_tcc(c_path: str, out_dll: str, include_dirs, flavor: int, '-DPSX_NO_DEBUG_TOOLS', '-DPSX_ENABLE_BLOCK_CYCLES=1', f'-DPSX_OVERLAY_FLAVOR={int(flavor)}', + # PGXP flavor bit (overlay_api.h PSX_OVERLAY_FLAVOR_PGXP): arm the + # PGXP_*() hook macros the emitter writes into every overlay C. + *(['-DPSX_PGXP=1'] if int(flavor) & 2 else []), native_path(c_path), '-o', native_path(out_dll)] for d in include_dirs: cmd.append('-I' + native_path(_bom_free_incdir(d))) @@ -4116,6 +4119,9 @@ def _compile_dll_direct(c_path: str, out_dll: str, include_dirs: list[str], # cache and a base cache can never cross-contaminate even if they share # a directory (they key by guest-bytes CRC, which is flavor-blind). f'-DPSX_OVERLAY_FLAVOR={int(flavor)}', + # PGXP flavor bit (overlay_api.h PSX_OVERLAY_FLAVOR_PGXP): arm the + # PGXP_*() hook macros the emitter writes into every overlay C. + *(['-DPSX_PGXP=1'] if int(flavor) & 2 else []), c_path, '-o', out_dll, *includes, @@ -5303,7 +5309,7 @@ def main(): ch = codegen_hash(args.runtime_include) gh = overlay_config_hash(args.recompiler, args.game_toml) cache_dir = os.path.join(args.out_dir, game_id, args.compiler, cache_arch_abi(), - f'cg{cg}_{ch:08x}_gc{gh:08x}') + f'cg{cg}_{ch:08x}_gc{gh:08x}_f{int(args.flavor)}') os.makedirs(cache_dir, exist_ok=True) print(f'Cache dir: {cache_dir} ' f'(codegen ver {cg}, emitter {ch:08x}, config {gh:08x})')