From e5e2dca85c2e7c1758c6de27870599103a1f8bfc Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 6 Oct 2026 22:18:08 -0700 Subject: [PATCH] Compile generated common contract bodies through wrappers for Ninja --- cmake/psx_execution_profile.cmake | 72 ++++++++++++++++--------- runtime/tests/test_execution_profile.py | 18 +++++-- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/cmake/psx_execution_profile.cmake b/cmake/psx_execution_profile.cmake index f3031131..402387bf 100644 --- a/cmake/psx_execution_profile.cmake +++ b/cmake/psx_execution_profile.cmake @@ -102,6 +102,31 @@ function(psxrecomp_execution_profile target) VERBATIM) endfunction() +# A materialized game shard can also be a configure/hash input. Compile an +# include wrapper so Ninja does not give the shard both a generation rule and +# CMake's input-file phony rule. Keep hashing the original body. +function(_psx_execution_compile_source target source output) + get_filename_component(_absolute "${source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + get_source_file_property(_generated "${_absolute}" GENERATED) + if(NOT _generated) + set(${output} "${source}" PARENT_SCOPE) + return() + endif() + string(SHA256 _path_hash "${_absolute}") + get_filename_component(_ext "${_absolute}" LAST_EXT) + set(_wrapper "${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}/sources/${_path_hash}${_ext}") + file(CONFIGURE OUTPUT "${_wrapper}" CONTENT "#include \"@_absolute@\"\n" @ONLY) + foreach(_property LANGUAGE COMPILE_DEFINITIONS COMPILE_OPTIONS COMPILE_FLAGS + INCLUDE_DIRECTORIES OBJECT_DEPENDS SKIP_PRECOMPILE_HEADERS + SKIP_UNITY_BUILD_INCLUSION) + get_source_file_property(_value "${_absolute}" "${_property}") + if(NOT _value STREQUAL "NOTFOUND") + set_source_files_properties("${_wrapper}" PROPERTIES "${_property}" "${_value}") + endif() + endforeach() + set(${output} "${_wrapper}" PARENT_SCOPE) +endfunction() + # Both source lists implement the same caller contract. Exclude these files # from common target sources; exactly one list is linked. CONTRACT_FILES names # adapter headers/recipes whose changes also affect the implementation identity. @@ -134,34 +159,29 @@ function(psxrecomp_add_implementation target) endif() list(APPEND _inputs "${_absolute}") endforeach() + # CONTRACT_FILES can include common generated game bodies already added by + # runtime.cmake, not only selected replacement bodies. Normalize those + # existing source entries before declaring their configure dependency. + get_target_property(_common_sources ${target} SOURCES) + set(_common_compile_sources "") + foreach(_source IN LISTS _common_sources) + if(_source MATCHES "^\\$<") + list(APPEND _common_compile_sources "${_source}") + continue() + endif() + get_filename_component(_absolute "${_source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + if(_absolute IN_LIST _inputs) + _psx_execution_compile_source(${target} "${_absolute}" _compiled) + list(APPEND _common_compile_sources "${_compiled}") + else() + list(APPEND _common_compile_sources "${_source}") + endif() + endforeach() + set_property(TARGET ${target} PROPERTY SOURCES "${_common_compile_sources}") set(_compile_sources "") foreach(_source IN LISTS _selected) - get_filename_component(_absolute "${_source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - get_source_file_property(_generated "${_absolute}" GENERATED) - if(_generated) - # runtime.cmake marks game shards GENERATED, though they are - # materialized before configure. Giving Ninja that file both as - # a compiled generated source and a configure dependency creates - # two output rules (an empty generation rule and a CMake phony). - # Compile an include wrapper instead. The original remains the - # sole maintained body and configure/hash input; #include keeps - # its relative includes and __FILE__ behavior intact. - string(SHA256 _path_hash "${_absolute}") - get_filename_component(_ext "${_absolute}" LAST_EXT) - set(_wrapper "${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}/sources/${_path_hash}${_ext}") - file(CONFIGURE OUTPUT "${_wrapper}" CONTENT "#include \"@_absolute@\"\n" @ONLY) - foreach(_property LANGUAGE COMPILE_DEFINITIONS COMPILE_OPTIONS COMPILE_FLAGS - INCLUDE_DIRECTORIES OBJECT_DEPENDS SKIP_PRECOMPILE_HEADERS - SKIP_UNITY_BUILD_INCLUSION) - get_source_file_property(_value "${_absolute}" "${_property}") - if(NOT _value STREQUAL "NOTFOUND") - set_source_files_properties("${_wrapper}" PROPERTIES "${_property}" "${_value}") - endif() - endforeach() - list(APPEND _compile_sources "${_wrapper}") - else() - list(APPEND _compile_sources "${_absolute}") - endif() + _psx_execution_compile_source(${target} "${_source}" _compiled) + list(APPEND _compile_sources "${_compiled}") endforeach() target_sources(${target} PRIVATE ${_compile_sources}) set_property(TARGET ${target} APPEND PROPERTY PSX_IMPLEMENTATION_FAMILIES "${I_NAME}") diff --git a/runtime/tests/test_execution_profile.py b/runtime/tests/test_execution_profile.py index a5b9cec5..27809fd2 100644 --- a/runtime/tests/test_execution_profile.py +++ b/runtime/tests/test_execution_profile.py @@ -28,7 +28,9 @@ def main(): #include "netplay_content_gate.h" #include extern int operation(int); +extern int common_game_body(void); int main(int argc, char** argv) { + if (common_game_body() != 5) return 4; char no_mods[65], modded[65], upper[65]; if (!psx_execution_content_identity("", no_mods) || !psx_execution_content_identity("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", modded) || @@ -49,6 +51,7 @@ int main(int argc, char** argv) { } ''') (source / "hle.c").write_text("int operation(int x) { return x * 3; }\n") + (source / "common.c").write_text("#ifndef COMMON_SOURCE_FLAG\n#error common source property lost\n#endif\nint common_game_body(void) { return 5; }\n") (source / "local_step.h").write_text("#define LOCAL_STEP 3\n") (source / "lle.c").write_text('#include "local_step.h"\n#ifndef LLE_SOURCE_FLAG\n#error source compile property lost\n#endif\nint operation(int x) { int s=0; while(x--) s+=LOCAL_STEP; return s; }\n') (source / "contract.h").write_text("/* caller contract revision 1 */\n") @@ -60,14 +63,16 @@ include("{root.as_posix()}/cmake/psx_execution_profile.cmake") # automatic configure/hash inputs without creating duplicate Ninja rules. set_source_files_properties("${{CMAKE_CURRENT_SOURCE_DIR}}/lle.c" PROPERTIES GENERATED TRUE COMPILE_DEFINITIONS LLE_SOURCE_FLAG=1) -add_executable(fixture main.c "{root.as_posix()}/runtime/src/psx_sha256.c") +set_source_files_properties("${{CMAKE_CURRENT_SOURCE_DIR}}/common.c" PROPERTIES + GENERATED TRUE COMPILE_DEFINITIONS COMMON_SOURCE_FLAG=1) +add_executable(fixture main.c common.c "{root.as_posix()}/runtime/src/psx_sha256.c") target_include_directories(fixture PRIVATE "{root.as_posix()}/runtime/include") -add_executable(fixture-pgxp main.c "{root.as_posix()}/runtime/src/psx_sha256.c") +add_executable(fixture-pgxp main.c common.c "{root.as_posix()}/runtime/src/psx_sha256.c") target_include_directories(fixture-pgxp PRIVATE "{root.as_posix()}/runtime/include") psxrecomp_execution_profile(fixture CONTRACT_FILES shared.h) psxrecomp_execution_profile(fixture-pgxp CONTRACT_FILES shared.h) psxrecomp_add_implementation(fixture NAME triple CONTRACT integer-v1 - HLE_SOURCES hle.c LLE_SOURCES lle.c CONTRACT_FILES contract.h) + HLE_SOURCES hle.c LLE_SOURCES lle.c CONTRACT_FILES contract.h common.c) ''') def configure(profile, name=None, ok=True): @@ -125,6 +130,13 @@ psxrecomp_add_implementation(fixture NAME triple CONTRACT integer-v1 assert changed_contract["identity"] != changed["identity"] (source / "shared.h").write_text("/* shared service revision 2 */\n") assert build_run(enhanced_build)["identity"] != changed_contract["identity"] + # Common generated bodies are compiled in both profiles and hashed as + # caller contracts. Their edits must also reconfigure automatically. + common_before = build_run(enhanced_build) + reference_before = build_run(Path(temp) / "REFERENCE") + (source / "common.c").write_text("int common_game_body(void) { return 2 + 3; }\n") + assert build_run(enhanced_build)["identity"] != common_before["identity"] + assert build_run(Path(temp) / "REFERENCE")["identity"] != reference_before["identity"] # Private HLE input is not required to build the maintained reference. (source / "hle.c").unlink() build_run(configure("REFERENCE", "reference-no-private"))