Files
psxrecomp/runtime/runtime.cmake
T

2983 lines
148 KiB
CMake
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Shared psxrecomp runtime CMake helpers.
#
# Include this from either the framework runtime build or a sibling game
# project. SDL3 is the default; set -DPSX_SDL_BACKEND=SDL2 for the legacy
# backend.
if(NOT DEFINED PSXRECOMP_ROOT)
get_filename_component(PSXRECOMP_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
endif()
# The rebuild CLI configures this as `generate` for its instrumented pass and
# `use` after it has merged the collected profile. Keep the switch here,
# beside the target it must affect: an otherwise unconsumed cache entry makes a
# costly PGO rebuild/train cycle a silent no-op.
set(PSX_PGO "" CACHE STRING "PGO mode: empty, generate, or use")
set_property(CACHE PSX_PGO PROPERTY STRINGS "" generate use)
include("${PSXRECOMP_ROOT}/cmake/psx_runtime_ipo.cmake")
include("${PSXRECOMP_ROOT}/cmake/psx_execution_profile.cmake")
include("${PSXRECOMP_ROOT}/cmake/psx_sha256_file.cmake")
include("${PSXRECOMP_ROOT}/cmake/psx_build_revision.cmake")
include("${PSXRECOMP_ROOT}/cmake/psx_dependency_archive.cmake")
include("${PSXRECOMP_ROOT}/cmake/psx_windows_manifest.cmake")
include("${PSXRECOMP_ROOT}/runtime/chd_dependency.cmake")
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
# runs at a small fraction of full speed (terrible framerate). A naive
# `cmake -B build` (as in the README) must NOT produce that, so default to
# Release when the user hasn't chosen a type. Single-config generators only;
# multi-config (VS/Xcode) pick per-build. Overridable with -DCMAKE_BUILD_TYPE=...
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING
"Build type (Release/RelWithDebInfo/Debug)" FORCE)
message(STATUS "psxrecomp: no CMAKE_BUILD_TYPE set — defaulting to Release "
"(optimized). Use -DCMAKE_BUILD_TYPE=RelWithDebInfo/Debug to override.")
endif()
# Content-addressed compiler cache (ccache). git branch operations (checkout /
# merge / new branch) rewrite working-tree file mtimes, which makes ninja treat
# the ~279 MB generated-C objects as stale and recompile them (~15 min) even when
# their content is byte-identical. ccache keys the object on the PREPROCESSED
# SOURCE + compiler + flags (content, not mtime), so those recompiles collapse to
# near-instant cache hits after any branch op. Completely no-op when ccache is not
# on PATH, so builds still work without it. Set once, before any target is added.
# Retro cmake-clang-v1 packs ship bin/ccache and prepend that dir to PATH;
# also HINT RETCOMM_TOOLCHAIN_DIR for wizards that only set the env override.
if(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
set(_psx_ccache_hints "")
if(DEFINED ENV{RETCOMM_TOOLCHAIN_DIR} AND NOT "$ENV{RETCOMM_TOOLCHAIN_DIR}" STREQUAL "")
list(APPEND _psx_ccache_hints "$ENV{RETCOMM_TOOLCHAIN_DIR}/bin")
endif()
# Only a Windows host can run ccache.exe. Under WSL the Windows PATH is
# appended to the Linux one, so a WinLibs ccache.exe would otherwise win
# and fail to exec a Linux (or osxcross) compiler.
if(CMAKE_HOST_WIN32)
set(_psx_ccache_names ccache ccache.exe)
else()
set(_psx_ccache_names ccache)
endif()
find_program(CCACHE_PROGRAM NAMES ${_psx_ccache_names} HINTS ${_psx_ccache_hints})
unset(_psx_ccache_names)
unset(_psx_ccache_hints)
if(CCACHE_PROGRAM)
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE STRING "compiler launcher")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE STRING "compiler launcher")
message(STATUS "psxrecomp: ccache enabled (${CCACHE_PROGRAM}) — mtime-proof rebuilds")
else()
message(STATUS "psxrecomp: ccache not found; generated-C rebuilds after git "
"branch ops will be slow. Install ccache on PATH (or update "
"cmake-clang-v1) to fix.")
endif()
endif()
# PSX_DEBUG_TOOLS: TCP debug server + heartbeat + per-block recording.
# Defaults ON for Debug/RelWithDebInfo, OFF for Release/MinSizeRel so
# a plain cmake -DCMAKE_BUILD_TYPE=Release gives a lean production binary
# with no TCP server and no debug console. Override explicitly with
# -DPSX_DEBUG_TOOLS=ON/OFF to force either way regardless of build type.
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
option(PSX_DEBUG_TOOLS "Build with TCP debug server + heartbeat + per-block recording" OFF)
else()
option(PSX_DEBUG_TOOLS "Build with TCP debug server + heartbeat + per-block recording" ON)
endif()
# A title may opt in only after auditing every trusted media preparer used by
# its mod commit. Older recomp-ui pins ignore this capability and remain sync.
option(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE
"Title certifies preboot mod commit/preparers have no UI or thread affinity" OFF)
# PSX_STATIC_RUNTIME: produce a 100% self-contained MinGW exe.
#
# A default MinGW build dynamically imports three NON-system DLLs —
# SDL.dll, libgcc_s_seh-1.dll, libstdc++-6.dll — which must be shipped
# next to the exe. On a user's machine that side-by-side scheme breaks
# when a different-architecture copy of one of those DLLs is found earlier
# on the DLL search path (System32, another app on PATH), producing the
# 0xc000007b STATUS_INVALID_IMAGE_FORMAT crash on launch.
#
# Linking those runtimes (and SDL) statically removes every non-system
# import, so the exe runs from any folder with zero bundled DLLs and the
# 0xc000007b failure mode becomes structurally impossible. Default ON for
# MinGW Release/MinSizeRel (the configs used to cut releases); override
# with -DPSX_STATIC_RUNTIME=OFF to force dynamic linking.
# zlib is folded the same way (static libz / zlibstatic) so Windows CI
# hosts do not import zlib1.dll — packagers still bundle it if a PE does.
if(MINGW AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
option(PSX_STATIC_RUNTIME "Statically link SDL + libgcc/libstdc++ for a self-contained exe" ON)
else()
option(PSX_STATIC_RUNTIME "Statically link SDL + libgcc/libstdc++ for a self-contained exe" OFF)
endif()
# SDL backend selection. SDL3 is fetched from an integrity-pinned stable
# release when no system package is available, so a default build does not
# silently fall back to SDL2. SDL2 remains an explicit, fully supported A/B
# backend.
# Dev-only: the lobby browser lists every version of this title instead of
# pinning to our exact game_version. Joining still requires an exact match.
option(PSX_LOBBY_DEV_LIST "Lobby browser ignores game_version (dev testing only)" OFF)
# Release channel (public | playtest | empty = dev) and the PSX_PUBLIC_RELEASE
# it derives: the one build-level notion of a public production release. Today
# PSX_PUBLIC_RELEASE selects Playtest Diagnostics defaults
# (runtime/playtest_diagnostics.cmake): OFF stages them default-on, ON keeps
# the source manifests' default-off. See runtime/psx_release_channel.cmake.
include("${CMAKE_CURRENT_LIST_DIR}/psx_release_channel.cmake")
set(PSX_SDL_BACKEND "SDL3" CACHE STRING "SDL backend (SDL3 or SDL2)")
set_property(CACHE PSX_SDL_BACKEND PROPERTY STRINGS SDL3 SDL2)
string(TOUPPER "${PSX_SDL_BACKEND}" _psx_sdl_backend)
if(NOT _psx_sdl_backend STREQUAL "SDL3" AND
NOT _psx_sdl_backend STREQUAL "SDL2")
message(FATAL_ERROR
"PSX_SDL_BACKEND must be SDL3 or SDL2 (got '${PSX_SDL_BACKEND}')")
endif()
set(PSX_SDL_INCLUDE_DIRS "")
set(PSX_SDL_LIBRARY_DIRS "")
set(PSX_SDL_LIBRARIES "")
set(PSX_SDL_STATIC_LDFLAGS "")
set(PSX_SDL3 OFF)
# Portable cmake-clang-v1 pack roots (wizard / Retro / CI emitter fetch).
# Collect as HINTS only — never list(PREPEND CMAKE_PREFIX_PATH …): on Windows CI
# the pack is llvm-mingw while the setup host links with MSYS2 g++, and a
# global prefix puts pack lib/ on the -L path so -static-libstdc++ can pick up
# the wrong libstdc++ (codecvt / filesystem undefined refs at link).
set(_PSX_TOOLCHAIN_PREFIX_HINTS "")
foreach(_psx_tc_env IN ITEMS RETCOMM_TOOLCHAIN_DIR PSXRECOMP_TOOLCHAIN_DIR
BPE_TOOLCHAIN_DIR TOOLCHAIN_DIR)
if(DEFINED ENV{${_psx_tc_env}} AND NOT "$ENV{${_psx_tc_env}}" STREQUAL "")
list(APPEND _PSX_TOOLCHAIN_PREFIX_HINTS "$ENV{${_psx_tc_env}}")
endif()
endforeach()
# The compiler's own location is the hint that cannot be forgotten. Callers
# that pass -DCMAKE_C_COMPILER=<toolchain>/bin/cc without also exporting one of
# the variables above — the setup wizard among them — used to leave this list
# empty, and every find_package below then fell through to the host's copy of a
# dependency. That is not merely a different copy: cmake-clang-v1 compiles
# against its own sysroot and never searches /usr/include, so a host package is
# found, reported as "using prebuilt/system", and then fails to compile.
#
# The exception is a compiler that lives in the *host* prefix. A distro cross
# compiler does: /usr/bin/x86_64-w64-mingw32-gcc yields /usr, whose
# lib/cmake and include/ hold Linux packages that compiler cannot use. Hinting
# there is this block's own failure inverted — the hints below are consumed by
# setting SDL3_DIR / ZLIB_ROOT outright, which bypasses the CMAKE_FIND_ROOT_PATH
# the cross toolchain file set precisely to keep host packages out. It is also
# the one hint that buys nothing: a build that *can* use the host prefix already
# searches it by default. So drop it and let find_package decide.
set(_PSX_HOST_PREFIXES "/" "/usr" "/usr/local" "/opt/local" "/opt/homebrew")
if(CMAKE_C_COMPILER)
get_filename_component(_psx_cc_bin "${CMAKE_C_COMPILER}" DIRECTORY)
get_filename_component(_psx_cc_pfx "${_psx_cc_bin}" DIRECTORY)
if(_psx_cc_pfx AND EXISTS "${_psx_cc_pfx}"
AND NOT _psx_cc_pfx IN_LIST _PSX_HOST_PREFIXES)
list(APPEND _PSX_TOOLCHAIN_PREFIX_HINTS "${_psx_cc_pfx}")
endif()
unset(_psx_cc_bin)
unset(_psx_cc_pfx)
endif()
unset(_PSX_HOST_PREFIXES)
list(REMOVE_DUPLICATES _PSX_TOOLCHAIN_PREFIX_HINTS)
# A dependency found outside the compiler's sysroot may still be unusable: the
# toolchain will not search it, and for headers under /usr/include CMake cannot
# even pass -I, because it drops that directory from include lists to avoid
# disturbing the system header order. So a "found" package has to be compiled
# before it is believed. Sets <out> to TRUE/FALSE.
include(CheckIncludeFile)
# INCLUDES for a bare header directory, LIBRARIES for an imported target — an
# imported target must be asked through CMAKE_REQUIRED_LIBRARIES rather than by
# reading INTERFACE_INCLUDE_DIRECTORIES off it, because a package may carry its
# headers on a transitive target. SDL3::SDL3 does exactly that (its include
# dirs live on SDL3::Headers), so reading the property yields NOTFOUND and the
# check would fail against a perfectly good SDL3.
function(_psx_header_compiles out header)
cmake_parse_arguments(_psx_hc "" "" "INCLUDES;LIBRARIES" ${ARGN})
set(CMAKE_REQUIRED_INCLUDES ${_psx_hc_INCLUDES})
set(CMAKE_REQUIRED_LIBRARIES "")
foreach(_library IN LISTS _psx_hc_LIBRARIES)
# Older CMake try_compile cannot export an alias of a local imported
# target (system SDL3 on Ubuntu). Pass its underlying imported target.
if(TARGET "${_library}")
get_target_property(_alias "${_library}" ALIASED_TARGET)
if(_alias)
set(_library "${_alias}")
endif()
endif()
list(APPEND CMAKE_REQUIRED_LIBRARIES "${_library}")
endforeach()
set(CMAKE_REQUIRED_QUIET ON)
string(MAKE_C_IDENTIFIER
"_psx_have_${header}_${_psx_hc_INCLUDES}_${_psx_hc_LIBRARIES}"
_cache_var)
check_include_file("${header}" ${_cache_var})
if(${_cache_var})
set(${out} TRUE PARENT_SCOPE)
else()
set(${out} FALSE PARENT_SCOPE)
endif()
endfunction()
if(_psx_sdl_backend STREQUAL "SDL3")
set(PSX_SDL3 ON)
option(PSX_SDL3_FETCH
"Fetch the pinned SDL3 release when no system SDL3 package is found"
ON)
# cmake-clang-v1 1.0.9+ ships SDL3 under deps/; 1.0.7–1.0.8 used pack root.
# Prefer SDL3_DIR / HINTS — never CMAKE_PREFIX_PATH=pack (mingw include poisons libc++).
if(NOT SDL3_DIR)
foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS)
foreach(_psx_sdl_root IN ITEMS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}")
if(EXISTS "${_psx_sdl_root}/lib/cmake/SDL3/SDL3Config.cmake" OR
EXISTS "${_psx_sdl_root}/lib/cmake/SDL3/SDL3-config.cmake")
set(SDL3_DIR "${_psx_sdl_root}/lib/cmake/SDL3")
break()
endif()
endforeach()
if(SDL3_DIR)
break()
endif()
endforeach()
endif()
set(_PSX_SDL3_HINTS "")
foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS)
list(APPEND _PSX_SDL3_HINTS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}")
endforeach()
find_package(SDL3 3.4 CONFIG QUIET COMPONENTS SDL3
HINTS ${_PSX_SDL3_HINTS}
PATH_SUFFIXES lib/cmake/SDL3)
unset(_PSX_SDL3_HINTS)
if(TARGET SDL3::SDL3)
# Trust it only if the compiler can actually read its headers. A host
# SDL3 under /usr/include satisfies find_package and then fails every
# translation unit that includes psx_sdl.h.
_psx_header_compiles(_psx_sdl3_ok "SDL3/SDL.h" LIBRARIES SDL3::SDL3)
if(NOT _psx_sdl3_ok)
message(FATAL_ERROR
"psxrecomp: SDL3 was found at ${SDL3_DIR} but <SDL3/SDL.h> does "
"not compile with ${CMAKE_C_COMPILER}.\n"
"That compiler searches its own sysroot, not the host's "
"/usr/include, so this SDL3 cannot be used even though CMake "
"located it.\n"
"Fix: point SDL3_DIR at the copy shipped with the toolchain, "
"e.g. -DSDL3_DIR=<toolchain>/deps/lib/cmake/SDL3, or configure "
"with -DPSX_SDL_BACKEND=SDL2, or build with a compiler that "
"sees the host headers.")
endif()
unset(_psx_sdl3_ok)
message(STATUS "psxrecomp: using prebuilt/system SDL3 (skip FetchContent)")
endif()
if(NOT TARGET SDL3::SDL3 AND PSX_SDL3_FETCH)
include(FetchContent)
# The fetched dependency is private to this build, so link it directly
# instead of producing an SDL3 DLL that would need platform-specific
# staging beside every generated game executable.
set(SDL_SHARED OFF CACHE BOOL "Build SDL3 shared library" FORCE)
set(SDL_STATIC ON CACHE BOOL "Build SDL3 static library" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "Build SDL3 test library" FORCE)
set(SDL_TESTS OFF CACHE BOOL "Build SDL3 tests" FORCE)
set(SDL_EXAMPLES OFF CACHE BOOL "Build SDL3 examples" FORCE)
set(_psx_sdl3_timestamp_args "")
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
list(APPEND _psx_sdl3_timestamp_args
DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
endif()
# CI (tools/ci/prefetch_sdl3.sh) pre-extracts with curl --http1.1 to
# avoid intermittent GitHub HTTP/2 REFUSED_STREAM failures from
# CMake's file(DOWNLOAD). Prefer that tree when present.
psxrecomp_dependency_source_dir(SDL3
ENV PSX_SDL3_SOURCE_DIR
OUT _psx_sdl3_src)
# Pin (and any vendored archive) come from third_party/deps.manifest, so
# an air-gapped tree staged by tools/ci/vendor_deps.sh needs no download.
psxrecomp_dependency_archive(SDL3
SOURCE_DIR "${_psx_sdl3_src}"
OUT_URL _psx_sdl3_url OUT_HASH _psx_sdl3_hash)
FetchContent_Declare(SDL3
URL
"${_psx_sdl3_url}"
URL_HASH
"${_psx_sdl3_hash}"
${_psx_sdl3_timestamp_args})
FetchContent_MakeAvailable(SDL3)
unset(_psx_sdl3_src)
unset(_psx_sdl3_url)
unset(_psx_sdl3_hash)
endif()
if(NOT TARGET SDL3::SDL3)
message(FATAL_ERROR
"SDL3 3.4+ was not found. Install SDL3, provide SDL3_DIR, or "
"configure with -DPSX_SDL3_FETCH=ON.")
endif()
if(PSX_STATIC_RUNTIME AND TARGET SDL3::SDL3-static)
set(PSX_SDL_LIBRARIES SDL3::SDL3-static)
else()
set(PSX_SDL_LIBRARIES SDL3::SDL3)
endif()
message(STATUS "psxrecomp: SDL backend = SDL3")
else()
if(NOT SDL2_INCLUDE_DIRS OR NOT SDL2_LIBRARIES)
if(MSVC)
file(GLOB SDL2_MSVC_DIR "${PSXRECOMP_ROOT}/../sdl2-msvc/SDL2-*")
if(SDL2_MSVC_DIR)
set(SDL2_INCLUDE_DIRS "${SDL2_MSVC_DIR}/include")
set(SDL2_LIBRARIES "${SDL2_MSVC_DIR}/lib/x64/SDL2.lib")
message(STATUS "SDL2 MSVC: ${SDL2_MSVC_DIR}")
else()
message(FATAL_ERROR "SDL2 MSVC dev package not found")
endif()
else()
get_filename_component(
_psxrecomp_compiler_dir "${CMAKE_C_COMPILER}" DIRECTORY)
find_program(_psxrecomp_pkg_config pkg-config
HINTS "${_psxrecomp_compiler_dir}"
NO_DEFAULT_PATH)
if(_psxrecomp_pkg_config)
set(PKG_CONFIG_EXECUTABLE "${_psxrecomp_pkg_config}"
CACHE FILEPATH "pkg-config executable" FORCE)
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED sdl2)
endif()
endif()
set(PSX_SDL_INCLUDE_DIRS "${SDL2_INCLUDE_DIRS}")
set(PSX_SDL_LIBRARY_DIRS "${SDL2_LIBRARY_DIRS}")
set(PSX_SDL_LIBRARIES "${SDL2_LIBRARIES}")
set(PSX_SDL_STATIC_LDFLAGS "${SDL2_STATIC_LDFLAGS}")
message(STATUS "psxrecomp: SDL backend = SDL2 (explicit fallback)")
endif()
# PSX_RECOMP_UI: wire the shared Dear ImGui launcher from the *game* repo's
# root recomp-ui submodule (CMAKE_SOURCE_DIR/recomp-ui). Not vendored in
# psxrecomp — games that need the launcher own the pin.
option(PSX_RECOMP_UI "Build the shared recomp-ui Dear ImGui launcher" ON)
option(PSX_SHELLWIN_INTERP "Default the shell-window dirty-RAM interpreter to ON ( BIOS without shell seeds )" OFF)
set(RECOMP_UI_ROOT "" CACHE PATH
"Path to recomp-ui; empty = <game>/recomp-ui")
if(PSX_RECOMP_UI AND (NOT RECOMP_UI_ROOT OR RECOMP_UI_ROOT STREQUAL ""))
if(EXISTS "${CMAKE_SOURCE_DIR}/recomp-ui/recomp_ui.cmake")
set(RECOMP_UI_ROOT "${CMAKE_SOURCE_DIR}/recomp-ui" CACHE PATH
"Path to recomp-ui; empty = <game>/recomp-ui" FORCE)
endif()
endif()
set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/main.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_window_icon.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_sdl_audio.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_stick.c
${PSXRECOMP_ROOT}/runtime/src/psx_jogcon_ffb_sdl.cpp
${PSXRECOMP_ROOT}/runtime/src/local_mouse_policy.cpp
${PSXRECOMP_ROOT}/runtime/src/local_mouse_sdl.cpp
${PSXRECOMP_ROOT}/runtime/src/memory.c
${PSXRECOMP_ROOT}/runtime/src/psx_ram_geometry.c
${PSXRECOMP_ROOT}/runtime/src/kernel_patch_ranges.c
${PSXRECOMP_ROOT}/runtime/src/guest_tty.c
${PSXRECOMP_ROOT}/runtime/src/gpu.c
${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c
${PSXRECOMP_ROOT}/runtime/src/ws_aspect_cone_math.c
${PSXRECOMP_ROOT}/runtime/src/gpu_sw_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
${PSXRECOMP_ROOT}/runtime/src/render_thread.c
${PSXRECOMP_ROOT}/runtime/src/present_thread.c
${PSXRECOMP_ROOT}/runtime/src/frame_gen.c
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_lle.c
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_hle.c
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_select.c
${PSXRECOMP_ROOT}/runtime/src/texture_pack_install.cpp
${PSXRECOMP_ROOT}/runtime/src/gpu_hd_textures.cpp
${PSXRECOMP_ROOT}/runtime/src/hd_texture_pack.cpp
${PSXRECOMP_ROOT}/runtime/src/duckstation_texture_pack.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
${PSXRECOMP_ROOT}/runtime/src/host_overlay.c
${PSXRECOMP_ROOT}/runtime/src/psx_menu_host.c
${PSXRECOMP_ROOT}/runtime/src/mod_pad_transform.c
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
${PSXRECOMP_ROOT}/runtime/src/dma_gpu_ll.c
${PSXRECOMP_ROOT}/runtime/src/dma.c
${PSXRECOMP_ROOT}/runtime/src/mdec.c
${PSXRECOMP_ROOT}/runtime/src/timers.c
${PSXRECOMP_ROOT}/runtime/src/interrupts.c
${PSXRECOMP_ROOT}/runtime/src/psx_video_timing.c
${PSXRECOMP_ROOT}/runtime/src/frame_pacing.c
${PSXRECOMP_ROOT}/runtime/src/frame_interpolation.c
${PSXRECOMP_ROOT}/runtime/src/render_pass.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_plan.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_motion.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_projection.cpp
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
${PSXRECOMP_ROOT}/runtime/src/dynamic_resolution.c
${PSXRECOMP_ROOT}/runtime/src/host_time.c
${PSXRECOMP_ROOT}/runtime/src/game_mode.c
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
${PSXRECOMP_ROOT}/runtime/src/sio.c
${PSXRECOMP_ROOT}/runtime/src/memcard.c
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
${PSXRECOMP_ROOT}/runtime/src/host_sampler.c
${PSXRECOMP_ROOT}/runtime/src/host_span_ring.c
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
${PSXRECOMP_ROOT}/runtime/src/draw_distance.c
${PSXRECOMP_ROOT}/runtime/src/mod_immediate.c
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
${PSXRECOMP_ROOT}/runtime/src/psx_segment_miss.c
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
${PSXRECOMP_ROOT}/runtime/src/text_xlate.cpp
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
${PSXRECOMP_ROOT}/runtime/src/device_trace.c
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
${PSXRECOMP_ROOT}/runtime/src/netplay_snap_ring.c
${PSXRECOMP_ROOT}/runtime/src/netplay_state_digest.c
${PSXRECOMP_ROOT}/runtime/src/netplay_input_hist.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay_rb.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay_sched.c
${PSXRECOMP_ROOT}/runtime/src/psx_selfcheck.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle_plan.c
${PSXRECOMP_ROOT}/runtime/src/savestate.c
${PSXRECOMP_ROOT}/runtime/src/video_telemetry.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_savestate_menu.c
${PSXRECOMP_ROOT}/runtime/src/psx_rewind.c
${PSXRECOMP_ROOT}/runtime/src/host_osd.c
${PSXRECOMP_ROOT}/runtime/src/psx_disc_tray.c
${PSXRECOMP_ROOT}/runtime/src/psx_runtime_menu.c
${PSXRECOMP_ROOT}/runtime/src/psx_host_menu.c
${PSXRECOMP_ROOT}/runtime/src/host_keymap.c
${PSXRECOMP_ROOT}/runtime/src/cosim_state.c
${PSXRECOMP_ROOT}/runtime/src/cosim.c
${PSXRECOMP_ROOT}/runtime/src/traps.c
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
${PSXRECOMP_ROOT}/runtime/src/freeze_dump_policy.c
${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c
${PSXRECOMP_ROOT}/runtime/src/gte.cpp
${PSXRECOMP_ROOT}/runtime/src/projection_scale_config.cpp
${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp
${PSXRECOMP_ROOT}/runtime/src/pgxp_session.cpp
${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c
${PSXRECOMP_ROOT}/runtime/src/crc32.c
${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c
${PSXRECOMP_ROOT}/runtime/src/disc_identity.cpp
${PSXRECOMP_ROOT}/runtime/src/cue_sheet.cpp
${PSXRECOMP_ROOT}/runtime/src/disc_path.cpp
${PSXRECOMP_ROOT}/runtime/src/cdrom.c
${PSXRECOMP_ROOT}/runtime/src/spu.c
${PSXRECOMP_ROOT}/runtime/src/spu_shadow.c
${PSXRECOMP_ROOT}/runtime/src/audio_shadow.c
${PSXRECOMP_ROOT}/runtime/src/audio_trace.c
${PSXRECOMP_ROOT}/runtime/src/color_lut.c
${PSXRECOMP_ROOT}/runtime/src/iso_reader.cpp
${PSXRECOMP_ROOT}/runtime/src/iso_reader_c.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_cycles.c
${PSXRECOMP_ROOT}/runtime/src/psx_icache.c
${PSXRECOMP_ROOT}/runtime/src/starvation_ring.c
${PSXRECOMP_ROOT}/runtime/src/guest_cycle_profile.c
${PSXRECOMP_ROOT}/runtime/src/latency_ring.c
${PSXRECOMP_ROOT}/runtime/src/frame_trace.c
${PSXRECOMP_ROOT}/runtime/src/stereo_shed_policy.c
${PSXRECOMP_ROOT}/runtime/src/present_image_ring.c
${PSXRECOMP_ROOT}/runtime/src/gpu_timeline.c
${PSXRECOMP_ROOT}/runtime/src/data_shards.c
${PSXRECOMP_ROOT}/runtime/src/load_accel.c
${PSXRECOMP_ROOT}/runtime/src/card_read_summary.c
${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c
${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/runtime/src/overlay_path_canon.c
${PSXRECOMP_ROOT}/runtime/src/overlay_posix.c
${PSXRECOMP_ROOT}/runtime/src/overlay_backend.c
${PSXRECOMP_ROOT}/runtime/src/autocompile.c
${PSXRECOMP_ROOT}/runtime/src/code_provider.c
${PSXRECOMP_ROOT}/runtime/src/event_ring.c
${PSXRECOMP_ROOT}/runtime/src/func_override.c
${PSXRECOMP_ROOT}/runtime/src/game_options.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_speed.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
${PSXRECOMP_ROOT}/runtime/src/mod_internal_resolution.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_ram.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_hd_textures.c
${PSXRECOMP_ROOT}/runtime/src/host_enhancements.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_crt_filter.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_image_filtering.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_enhanced_audio.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_rewind.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_internal_resolution.c
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_netplay.c
${PSXRECOMP_ROOT}/runtime/src/mod_resident.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_guest_services.c
${PSXRECOMP_ROOT}/runtime/src/mod_texture_banks.c
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
${PSXRECOMP_ROOT}/runtime/src/psx_bios_module.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay.c
${PSXRECOMP_ROOT}/runtime/src/psx_net_pad.c
${PSXRECOMP_ROOT}/runtime/src/psx_pad_apply.c
${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.c
${PSXRECOMP_ROOT}/runtime/src/netplay_bios_settle.c
${PSXRECOMP_ROOT}/runtime/src/netplay_exit_reason.c
${PSXRECOMP_ROOT}/runtime/src/netplay_ice_hub.c
${PSXRECOMP_ROOT}/recompiler/src/config_loader.cpp
${PSXRECOMP_ROOT}/recompiler/src/ps1_exe_parser.cpp
# (sljit Tier-2 in-process JIT backend removed 2026-07-15 — was disabled by
# default since 2026-06-25; gaps fall to the interpreter, gcc/tcc unaffected.)
)
# TCP `disasm` command only (debug_server.c); stripped with the rest of the TCP
# server when PSX_DEBUG_TOOLS is OFF.
if(PSX_DEBUG_TOOLS)
list(APPEND PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/recompiler/src/mips_decoder.cpp
${PSXRECOMP_ROOT}/runtime/src/disasm_shim.cpp)
endif()
# Optional delay-sync netplay (recomp-net). Auto-discovers a sibling checkout
# (…/recomp-net next to the game repo or next to psxrecomp). Override with
# -DRECOMP_NET_ROOT=… or -DPSX_NETPLAY=OFF.
# OFF by default: netplay is a per-title opt-in, not something every build
# carries. Most titles here are single-player, and an ON default silently
# pulled in the recomp-net library, the lobby WebSocket client and their link
# deps for games that can never use them. A multiplayer title opts in with
# -DPSX_NETPLAY=ON (or sets it before including this file).
option(PSX_NETPLAY "Link recomp-net delay-sync (opt-in; needs recomp-net)" OFF)
# First-run setup wizard + Generate & rebuild (recomp-ui). OFF by default so
# titles that have not tested the self-build flow do not advertise it. Opt in
# with -DPSX_SETUP_WIZARD=ON (or ENABLE_SETUP_WIZARD on psxrecomp_add_game_runtime
# after setting the cache before include, same pattern as PSX_NETPLAY).
option(PSX_SETUP_WIZARD
"Advertise first-run setup wizard + Generate & rebuild in recomp-ui" OFF)
set(RECOMP_NET_ROOT "" CACHE PATH "Path to recomp-net; empty = auto-discover")
if(PSX_NETPLAY AND NOT RECOMP_NET_ROOT)
foreach(_cand
"${PSXRECOMP_ROOT}/lib/recomp-net"
"${CMAKE_SOURCE_DIR}/../recomp-net"
"${PSXRECOMP_ROOT}/../recomp-net"
"${CMAKE_SOURCE_DIR}/recomp-net")
get_filename_component(_abs "${_cand}" ABSOLUTE)
if(EXISTS "${_abs}/CMakeLists.txt")
set(RECOMP_NET_ROOT "${_abs}" CACHE PATH "Path to recomp-net; empty = auto-discover" FORCE)
break()
endif()
endforeach()
endif()
if(PSX_NETPLAY AND RECOMP_NET_ROOT AND EXISTS "${RECOMP_NET_ROOT}/CMakeLists.txt")
if(NOT TARGET recomp_net)
set(RNET_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(RNET_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# MotK lobby ice_p2p needs libjuice. Default ON with netplay; no FORCE
# so -DRNET_ENABLE_ICE=OFF still wins (LAN-only / offline configure).
# recomp-net prefers URL FetchContent / third_party/libjuice over git
# clone (AppImage LD_LIBRARY_PATH breaks system git-remote-https).
set(RNET_ENABLE_ICE ON CACHE BOOL
"Build libjuice ICE transport (default ON with PSX_NETPLAY)")
psxrecomp_netplay_libjuice("${RECOMP_NET_ROOT}")
add_subdirectory("${RECOMP_NET_ROOT}" "${CMAKE_BINARY_DIR}/recomp-net")
endif()
set(PSXRECOMP_HAS_RECOMP_NET TRUE)
if(RNET_ENABLE_ICE)
message(STATUS "psxrecomp: recomp-net netplay+ICE enabled (${RECOMP_NET_ROOT})")
else()
message(STATUS "psxrecomp: recomp-net netplay enabled, ICE off (${RECOMP_NET_ROOT})")
endif()
else()
set(PSXRECOMP_HAS_RECOMP_NET FALSE)
if(PSX_NETPLAY)
message(STATUS "psxrecomp: recomp-net not found — netplay stubs only "
"(set RECOMP_NET_ROOT or place checkout at ../recomp-net)")
else()
message(STATUS "psxrecomp: PSX_NETPLAY=OFF — netplay TUs compile as stubs "
"(no recomp-net)")
endif()
endif()
# Portable rollback host policy (sched/hist/hash_confirm/snap). Sits next to
# recomp-net; MotK keeps thin PSX glue (pad↔frame, boot_state, FMV/dig0 gates).
# Snap ring is also used for local rewind without linking full rbengine/recomp-net.
set(RECOMP_RBENGINE_ROOT "" CACHE PATH "Path to retcomm-rbengine; empty = auto-discover")
if(NOT RECOMP_RBENGINE_ROOT)
foreach(_cand
"${PSXRECOMP_ROOT}/lib/retcomm-rbengine"
"${CMAKE_SOURCE_DIR}/../retcomm-rbengine"
"${PSXRECOMP_ROOT}/../retcomm-rbengine")
get_filename_component(_abs "${_cand}" ABSOLUTE)
if(EXISTS "${_abs}/include/retcomm_rbengine/snap_ring.h")
set(RECOMP_RBENGINE_ROOT "${_abs}" CACHE PATH
"Path to retcomm-rbengine; empty = auto-discover" FORCE)
break()
endif()
endforeach()
endif()
if(PSXRECOMP_HAS_RECOMP_NET AND RECOMP_RBENGINE_ROOT
AND EXISTS "${RECOMP_RBENGINE_ROOT}/CMakeLists.txt")
if(NOT TARGET retcomm_rbengine)
set(RBE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# recomp_net already added above; rbengine skips nested add_subdirectory.
set(RECOMP_NET_ROOT "${RECOMP_NET_ROOT}" CACHE PATH "" FORCE)
add_subdirectory("${RECOMP_RBENGINE_ROOT}"
"${CMAKE_BINARY_DIR}/retcomm-rbengine")
endif()
set(PSXRECOMP_HAS_RBENGINE TRUE)
message(STATUS "psxrecomp: retcomm-rbengine enabled (${RECOMP_RBENGINE_ROOT})")
else()
set(PSXRECOMP_HAS_RBENGINE FALSE)
if(PSXRECOMP_HAS_RECOMP_NET)
message(FATAL_ERROR
"psxrecomp: PSX_NETPLAY needs retcomm-rbengine.\n"
" git submodule update --init --recursive lib/retcomm-rbengine\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine")
endif()
endif()
# OpenGL textured-batch grouping tier (runtime/src/gl_batch_policy.h;
# recomp-template HLE.md). LLE, the reference, draws every semi-transparent
# primitive alone. HLE draws painter-ordered runs of opaque primitives and
# blend modes 0/1/3 as one dual-source draw. Build-time only: use separate
# build directories. Both tiers always compile.
set(PSX_GL_BATCH_IMPL "LLE" CACHE STRING "OpenGL textured batching: LLE or HLE")
set_property(CACHE PSX_GL_BATCH_IMPL PROPERTY STRINGS LLE HLE)
if(NOT PSX_GL_BATCH_IMPL MATCHES "^(LLE|HLE)$")
message(FATAL_ERROR "PSX_GL_BATCH_IMPL must be LLE or HLE")
endif()
message(STATUS "psxrecomp: GL textured batching = ${PSX_GL_BATCH_IMPL}")
# Local rewind: full rbengine when netplay is on; otherwise compile snap_ring.c
# only (no recomp-net / sched / hash_confirm). Never both — duplicate symbols.
# Defaults ON because local rewind is useful for every PSX title. If a title
# deliberately opts out with -DPSX_REWIND=OFF, recomp-ui hides the Rewind
# controls instead of exposing dead hotkeys.
option(PSX_REWIND "Build and expose local rewind support" ON)
set(PSXRECOMP_HAS_RBENGINE_SNAP FALSE)
set(PSXRECOMP_RBENGINE_SNAP_INCLUDE "")
if(PSX_REWIND)
if(PSXRECOMP_HAS_RBENGINE)
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
elseif(RECOMP_RBENGINE_ROOT
AND EXISTS "${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c"
AND EXISTS "${RECOMP_RBENGINE_ROOT}/include/retcomm_rbengine/snap_ring.h")
list(APPEND PSXRECOMP_RUNTIME_SOURCES
${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c)
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
set(PSXRECOMP_RBENGINE_SNAP_INCLUDE "${RECOMP_RBENGINE_ROOT}/include")
message(STATUS "psxrecomp: rewind snap_ring (${RECOMP_RBENGINE_ROOT})")
endif()
if(NOT PSXRECOMP_HAS_RBENGINE_SNAP)
message(FATAL_ERROR
"psxrecomp: PSX_REWIND=ON exposes the Rewind launcher controls "
"but no retcomm-rbengine snap-ring backend was found.\n"
"A source ZIP downloaded from GitHub never contains submodule "
"contents and cannot build. Clone instead:\n"
" git clone --recurse-submodules <repo-url>\n"
"In an existing clone, run this from the GAME repo root. Note "
"--recursive: rbengine is a submodule of psxrecomp, not of the "
"game, so a non-recursive init leaves it empty.\n"
" git submodule update --init --recursive\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine\n"
" or configure with -DPSX_REWIND=OFF to hide Rewind.")
endif()
else()
message(STATUS "psxrecomp: local rewind disabled (PSX_REWIND=OFF)")
endif()
# Lobby WebSocket client helpers are vendored under runtime/src/lobby_ws/
# (protocol talks to the proprietary recomp-net-server, not recomp-net).
set(PSXRECOMP_LOBBY_WS_DIR "${PSXRECOMP_ROOT}/runtime/src/lobby_ws")
# Gated on PSX_NETPLAY: this is the client for the netplay lobby server, so it
# is dead weight in a single-player build. It used to enable itself on nothing
# more than the source files existing, i.e. always.
if(PSX_NETPLAY
AND EXISTS "${PSXRECOMP_LOBBY_WS_DIR}/rnet_ws.c"
AND EXISTS "${PSXRECOMP_LOBBY_WS_DIR}/rnet_sha1.c")
set(PSXRECOMP_HAS_LOBBY_CLIENT TRUE)
list(APPEND PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_LOBBY_WS_DIR}/rnet_ws.c
${PSXRECOMP_LOBBY_WS_DIR}/rnet_sha1.c)
set(PSXRECOMP_LOBBY_INCLUDE_DIR "${PSXRECOMP_LOBBY_WS_DIR}")
message(STATUS "psxrecomp: lobby client enabled (${PSXRECOMP_LOBBY_WS_DIR})")
else()
set(PSXRECOMP_HAS_LOBBY_CLIENT FALSE)
set(PSXRECOMP_LOBBY_INCLUDE_DIR "")
endif()
set(PSXRECOMP_RUNTIME_INCLUDE_DIRS
${PSXRECOMP_ROOT}/runtime/include
${PSXRECOMP_ROOT}/recompiler/src
${PSXRECOMP_ROOT}/recompiler/include
${PSXRECOMP_ROOT}/recompiler/lib/fmt/include
${PSXRECOMP_ROOT}/recompiler/lib/toml11
)
if(PSXRECOMP_LOBBY_INCLUDE_DIR)
list(APPEND PSXRECOMP_RUNTIME_INCLUDE_DIRS ${PSXRECOMP_LOBBY_INCLUDE_DIR})
endif()
if(PSXRECOMP_RBENGINE_SNAP_INCLUDE)
list(APPEND PSXRECOMP_RUNTIME_INCLUDE_DIRS ${PSXRECOMP_RBENGINE_SNAP_INCLUDE})
endif()
# Which recompiled BIOSes the runtime links. A build carries every image it
# ships — bundled OpenBIOS plus a retail one — and chooses between them at
# startup (docs/BIOS_SELECTION.md). Each image exports a single
# <STEM>_psx_bios_backend descriptor and namespaces everything else, so they
# co-link; the runtime routes psx_dispatch()/psx_bios_image through whichever
# backend it selects.
#
# One build configuration on purpose: the previous per-BIOS flavour needed the
# same choice restated in game.toml and two CMake variables with nothing
# cross-checking them, and it linked cleanly when they disagreed.
set(PSXRECOMP_BUNDLED_BIOS_PATH "bios/openbios.bin" CACHE STRING
"Bundled redistributable BIOS image, relative to the executable")
set(PSXRECOMP_BUNDLED_BIOS_SOURCE "${PSXRECOMP_ROOT}/bios/openbios.bin" CACHE FILEPATH
"Source image copied into native runtime builds as the bundled BIOS")
set(PSXRECOMP_BUNDLED_BIOS_LICENSE "${PSXRECOMP_ROOT}/bios/OpenBIOS.LICENSE" CACHE FILEPATH
"License notice copied alongside the bundled BIOS")
set(PSXRECOMP_BIOS_STEMS "OpenBIOS;SCPH1001" CACHE STRING
"Recompiled BIOS stems to link (first bundled/redistributable one is the default at runtime)")
# The profile is still needed as the staleness-stamp input for the primary stem.
list(GET PSXRECOMP_BIOS_STEMS 0 PSXRECOMP_BIOS_STEM_PRIMARY)
set(PSXRECOMP_BIOS_STEM "${PSXRECOMP_BIOS_STEM_PRIMARY}" CACHE STRING
"Primary recompiled BIOS stem (staleness stamp; see PSXRECOMP_BIOS_STEMS)")
set(PSXRECOMP_BIOS_PROFILE "${PSXRECOMP_ROOT}/bios/${PSXRECOMP_BIOS_STEM}.toml" CACHE FILEPATH
"BIOS profile TOML this build regenerates from (staleness stamp input)")
# The setup host must test the same requested backends as the linker, including
# a relocated framework checkout. Keep the path relative for portable kits.
file(RELATIVE_PATH PSXRECOMP_SETUP_FRAMEWORK_REL "${CMAKE_SOURCE_DIR}" "${PSXRECOMP_ROOT}")
string(REPLACE ";" "|" PSXRECOMP_SETUP_BIOS_STEMS "${PSXRECOMP_BIOS_STEMS}")
set(PSXRECOMP_EXPECTED_RETAIL_STEM "")
foreach(_stem IN LISTS PSXRECOMP_BIOS_STEMS)
if(NOT _stem MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
message(FATAL_ERROR "Invalid BIOS backend stem: ${_stem}")
endif()
if(NOT _stem STREQUAL "OpenBIOS" AND NOT PSXRECOMP_EXPECTED_RETAIL_STEM)
set(PSXRECOMP_EXPECTED_RETAIL_STEM "${_stem}")
endif()
endforeach()
# Link a stem only if its generated sources are actually present.
#
# PSXRECOMP_BIOS_STEMS lists every stem this build WOULD like. SCPH1001 is in
# the default list, but its generated C is a derivative of a copyrighted Sony
# BIOS, so it is gitignored and only exists once a developer regenerates it
# from their own dump. Requiring it unconditionally meant a fresh checkout --
# which legitimately has only the bundled MIT OpenBIOS -- failed every game
# link with "undefined reference to SCPH1001_psx_bios_backend", long after
# configure had succeeded. Filtering here keeps SCPH1001 fully supported for
# anyone who has regenerated it, without making it mandatory for everyone else.
set(PSXRECOMP_BIOS_GENERATED "")
set(_psxrt_registry_externs "")
set(_psxrt_registry_entries "")
set(_psxrt_bios_linked "")
set(_psxrt_bios_skipped "")
foreach(_stem IN LISTS PSXRECOMP_BIOS_STEMS)
# Presence is not enough: a generated/ tree left over from before the
# backend-descriptor mechanism has both files but defines no descriptor,
# which links fine at configure time and then fails at link with an
# undefined reference. Probe the descriptor itself. It is emitted into
# <stem>_dispatch.c (~1MB), never the multi-megabyte <stem>_full.c, so
# this scan stays cheap.
set(_psxrt_desc "")
if(EXISTS "${PSXRECOMP_ROOT}/generated/${_stem}_dispatch.c")
file(STRINGS "${PSXRECOMP_ROOT}/generated/${_stem}_dispatch.c" _psxrt_desc
REGEX "${_stem}_psx_bios_backend" LIMIT_COUNT 1)
endif()
if(EXISTS "${PSXRECOMP_ROOT}/generated/${_stem}_full.c" AND _psxrt_desc)
list(APPEND PSXRECOMP_BIOS_GENERATED
${PSXRECOMP_ROOT}/generated/${_stem}_full.c
${PSXRECOMP_ROOT}/generated/${_stem}_dispatch.c)
string(APPEND _psxrt_registry_externs
"extern const PsxBiosBackend ${_stem}_psx_bios_backend;
")
string(APPEND _psxrt_registry_entries
" &${_stem}_psx_bios_backend,
")
list(APPEND _psxrt_bios_linked "${_stem}")
else()
list(APPEND _psxrt_bios_skipped "${_stem}")
endif()
endforeach()
if(_psxrt_bios_skipped)
message(STATUS
"BIOS backends skipped (generated C missing or predates the backend "
"descriptor): ${_psxrt_bios_skipped} -- regenerate with "
"tools/regen_bios.sh --config bios/<stem>.toml")
endif()
# Setup hosts / CI may ship with zero BIOS backends; first-run Generate &
# rebuild emits OpenBIOS (and optional retail) locally before a full link.
option(PSXRECOMP_ALLOW_NO_BIOS
"Allow linking the runtime with no recompiled BIOS backends (setup host)"
OFF)
if(NOT _psxrt_bios_linked)
if(PSXRECOMP_ALLOW_NO_BIOS)
message(STATUS
"BIOS backends linked: (none) — setup host; Generate & rebuild "
"will emit BIOS C locally")
set(_psxrt_bios_count 0)
else()
message(FATAL_ERROR
"No recompiled BIOS backend available. Wanted: ${PSXRECOMP_BIOS_STEMS}, "
"but no matching generated/<stem>_full.c + <stem>_dispatch.c were found "
"under ${PSXRECOMP_ROOT}/generated.\n"
"Generate at least one before building the runtime:\n"
" bash tools/regen_bios.sh --config bios/OpenBIOS.toml\n"
"(OpenBIOS is bundled and MIT-licensed, so this needs no BIOS dump.)\n"
"Or configure a setup host with -DPSXRECOMP_ALLOW_NO_BIOS=ON.")
endif()
else()
message(STATUS "BIOS backends linked: ${_psxrt_bios_linked}")
list(LENGTH _psxrt_bios_linked _psxrt_bios_count)
endif()
# ISO C requires at least one initializer. A setup host can intentionally have
# zero linked BIOS backends, so give the generated array one unused null entry
# while keeping its public count at zero. MSVC 19.44 otherwise crashes with
# C1001 in CloseTypeServerPDB when it compiles an empty initializer list.
if(NOT _psxrt_registry_entries)
set(_psxrt_registry_entries " 0, /* unused: registry count is zero */
")
endif()
# Registry of the compiled-in backends, in preference order. Generated so the
# stem list stays the single source of truth.
set(_psxrt_registry_c "${CMAKE_BINARY_DIR}/psx_bios_registry.c")
file(WRITE "${_psxrt_registry_c}"
"/* Generated by runtime.cmake from PSXRECOMP_BIOS_STEMS — do not edit. */
"
"#include \"psx_bios_backend.h\"
"
"${_psxrt_registry_externs}"
"
const PsxBiosBackend *const psx_bios_builtin_registry[] = {
"
"${_psxrt_registry_entries}"
"};
"
"const uint32_t psx_bios_builtin_registry_count = ${_psxrt_bios_count}u;
")
list(APPEND PSXRECOMP_BIOS_GENERATED "${_psxrt_registry_c}")
# --- BIOS generated/ staleness check (hygiene) -----------------------------------
# generated/<stem>_*.c is gitignored build output produced by a SEPARATE build
# (recompiler/ -> psxrecomp-bios). Editing the BIOS emitter without re-running
# tools/regen_bios.sh leaves the runtime linking a stale BIOS that no longer matches
# the emitter (this caused a 4439-vs-4406 drift). regen_bios.sh records an emitter
# fingerprint in generated/<stem>.emitter.sha; recompute it here (same profile
# argument as regen_bios.sh passes) and WARN on a mismatch so the staleness is
# impossible to miss. Non-fatal by default: a stale-but-consistent BIOS still
# builds; opt out with -DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON. Release CI passes
# -DPSXRECOMP_BIOS_STALE_FATAL=ON: the BIOS C is COMMITTED in generated/, so a
# stamp that predates the emitter means a release would link C the current
# emitter did not produce (the 2026-09-30 descriptor drift was exactly that,
# caught only at compile). Every LINKED stem is checked, not just the primary.
option(PSXRECOMP_BIOS_STALE_FATAL
"Fail configure when a linked BIOS stem's generated/ stamp predates the emitter"
OFF)
# The fingerprint is a CMake script run with THIS cmake, never a bash found on
# PATH: on the windows-2022 runner the bash CMake located hashed differently
# from the step shell's, and a byte-identical tree read as STALE there only.
if(NOT PSXRECOMP_SKIP_BIOS_STALE_CHECK AND _psxrt_bios_linked)
if(EXISTS "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.cmake")
foreach(_psxrt_chk_stem IN LISTS _psxrt_bios_linked)
set(_psxrt_stamp "${PSXRECOMP_ROOT}/generated/${_psxrt_chk_stem}.emitter.sha")
set(_psxrt_chk_profile "${PSXRECOMP_ROOT}/bios/${_psxrt_chk_stem}.toml")
if(NOT EXISTS "${_psxrt_chk_profile}")
continue()
endif()
execute_process(
COMMAND "${CMAKE_COMMAND}" "-DROOT=${PSXRECOMP_ROOT}"
"-DPROFILE=${_psxrt_chk_profile}"
-P "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.cmake"
WORKING_DIRECTORY "${PSXRECOMP_ROOT}"
OUTPUT_VARIABLE _psxrt_cur_fp OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE _psxrt_fp_rc ERROR_QUIET)
if(_psxrt_fp_rc EQUAL 0 AND _psxrt_cur_fp)
set(_psxrt_saved_fp "")
if(EXISTS "${_psxrt_stamp}")
file(READ "${_psxrt_stamp}" _psxrt_saved_fp)
string(STRIP "${_psxrt_saved_fp}" _psxrt_saved_fp)
endif()
if(NOT _psxrt_saved_fp STREQUAL _psxrt_cur_fp)
if(PSXRECOMP_BIOS_STALE_FATAL)
set(_psxrt_stale_sev FATAL_ERROR)
else()
set(_psxrt_stale_sev WARNING)
endif()
if(_psxrt_chk_stem MATCHES "^SCPH")
set(_psxrt_stale_publish
"Retail dump/generated outputs stay local and gitignored; do not commit them.")
else()
set(_psxrt_stale_publish
"Commit the regenerated ${_psxrt_chk_stem}_*.c and .emitter.sha together.")
endif()
message(${_psxrt_stale_sev}
"BIOS generated/${_psxrt_chk_stem}_*.c is STALE vs the recompiler "
"emitter (fingerprint mismatch).\n"
" stamp ${_psxrt_saved_fp}\n"
" now ${_psxrt_cur_fp}\n"
" The committed C may not match the current emitter source, "
"seeds or profile.\n"
" Fix: tools/regen_bios.sh --config bios/${_psxrt_chk_stem}.toml "
"\n ${_psxrt_stale_publish}\n"
" (Suppress locally: -DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON)")
endif()
endif()
endforeach()
endif()
endif()
# zlib for boot_state v4. System packages first; otherwise FetchContent a pinned
# release so portable Windows cmake-clang-v1 setups can configure without MSYS2.
option(PSX_ZLIB_FETCH
"Fetch zlib when no system ZLIB package is found"
ON)
function(psxrecomp_ensure_zlib)
if(TARGET ZLIB::ZLIB)
return()
endif()
# Prefer pack zlib via ZLIB_ROOT — deps/ (1.0.9+) then legacy pack root.
# Never use CMAKE_PREFIX_PATH=pack on Windows llvm-mingw (libc++ clash).
if(NOT ZLIB_ROOT)
foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS)
foreach(_psx_z_root IN ITEMS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}")
if(EXISTS "${_psx_z_root}/include/zlib.h" AND
(EXISTS "${_psx_z_root}/lib/libz.a" OR
EXISTS "${_psx_z_root}/lib/zlib.lib"))
set(ZLIB_ROOT "${_psx_z_root}")
break()
endif()
endforeach()
if(ZLIB_ROOT)
break()
endif()
endforeach()
endif()
# PSX_STATIC_RUNTIME promises no non-system DLL imports. MSYS2's
# ZLIB::ZLIB is usually shared (zlib1.dll); find_package would leave
# that import in the host even with -static-libgcc.
set(_psx_zlib_saved_suffixes "")
if(PSX_STATIC_RUNTIME)
set(ZLIB_USE_STATIC_LIBS ON)
if(MINGW)
set(_psx_zlib_saved_suffixes "${CMAKE_FIND_LIBRARY_SUFFIXES}")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
endif()
endif()
find_package(ZLIB QUIET)
if(NOT "${_psx_zlib_saved_suffixes}" STREQUAL "")
set(CMAKE_FIND_LIBRARY_SUFFIXES "${_psx_zlib_saved_suffixes}")
endif()
if(TARGET ZLIB::ZLIB)
if(PSX_STATIC_RUNTIME)
message(STATUS "psxrecomp: ZLIB static (PSX_STATIC_RUNTIME)")
endif()
return()
endif()
if(NOT PSX_ZLIB_FETCH)
message(FATAL_ERROR
"ZLIB was not found. On Windows, use a cmake-clang-v1 pack that "
"ships deps/include/zlib.h + deps/lib/libz.a (sets ZLIB_ROOT), or "
"install zlib-devel / mingw-w64-zlib, or configure with "
"-DPSX_ZLIB_FETCH=ON.")
endif()
message(STATUS
"psxrecomp: ZLIB not in ZLIB_ROOT / system paths; "
"fetching zlib 1.3.1 (prefer a toolchain pack that bundles it)")
include(FetchContent)
set(_psx_zlib_timestamp_args "")
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
list(APPEND _psx_zlib_timestamp_args DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
endif()
psxrecomp_dependency_source_dir(psx_zlib
ENV PSX_ZLIB_SOURCE_DIR
OUT _psx_zlib_src)
psxrecomp_dependency_archive(psx_zlib
SOURCE_DIR "${_psx_zlib_src}"
OUT_URL _psx_zlib_url OUT_HASH _psx_zlib_hash)
FetchContent_Declare(psx_zlib
URL
"${_psx_zlib_url}"
URL_HASH
"${_psx_zlib_hash}"
${_psx_zlib_timestamp_args})
FetchContent_MakeAvailable(psx_zlib)
# madler/zlib builds zlibstatic even when a shared zlib target exists.
# Prefer static when PSX_STATIC_RUNTIME so the host does not import zlib1.dll.
if(PSX_STATIC_RUNTIME AND TARGET zlibstatic AND NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
message(STATUS "psxrecomp: ZLIB via FetchContent (zlibstatic)")
return()
endif()
if(TARGET ZLIB::ZLIB)
message(STATUS "psxrecomp: ZLIB via FetchContent (ZLIB::ZLIB)")
return()
endif()
if(TARGET zlibstatic)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
message(STATUS "psxrecomp: ZLIB via FetchContent (zlibstatic)")
return()
endif()
if(TARGET zlib)
add_library(ZLIB::ZLIB ALIAS zlib)
message(STATUS "psxrecomp: ZLIB via FetchContent (zlib)")
return()
endif()
message(FATAL_ERROR
"Fetched zlib but no linkable target was produced "
"(expected zlibstatic or zlib).")
endfunction()
# ---------------------------------------------------------------------------
# Mod catalog staging -- THE FRAMEWORK OWNS THE LAYOUT
# ---------------------------------------------------------------------------
# docs/MOD_PACKAGES.md defines <exe-dir>/mods/bundled as "build output -- the
# framework's mods/builtin/packages plus the title's mods/preloaded/packages".
# Until now only the first half of that sentence was implemented here, and the
# second half was implemented five times, once per title, as a hand-written
#
# add_custom_command(TARGET psx-runtime POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_directory
# "${<TITLE>_PRELOADED_MODS}" "$<TARGET_FILE_DIR:psx-runtime>/mods")
#
# in ApeEscapeRecomp, Tomba2Recomp, MegaManX4/5/6Recomp. Because those blocks
# name the destination as a STRING, framework commit 4cc04be3 -- which renamed
# the staged directory from mods/packages to mods/bundled so a rebuild could
# stop deleting the player's installed mods -- broke all five without breaking
# anything a compiler, linker or configure step can see. The titles kept
# writing mods/packages; the launcher and the release packager had moved to
# mods/bundled. Discovery was deferred to whichever title next ran a release
# packager (bead beads-eio.3.101).
#
# So the layout string now appears in exactly one place. A title declares WHERE
# its packages live (PRELOADED_MODS_DIR) and never HOW they are laid out, and a
# future rename touches this file only.
#
# Two guards keep it that way:
# * configure time -- a title that has mods/preloaded/packages in its source
# tree but did not declare it is a hard configure error, because that is
# exactly the missed-title state, and it is detectable before any build.
# * build time -- a small catalog-staging target runs before its game target.
# It re-stages only package files, never recompiles or relinks the game,
# then runs runtime/psx_check_mod_catalog.cmake and fails if a declared
# package did not reach mods/bundled, or if any package this build stages
# turned up in the legacy mods/packages instead.
#
# A title may also decline individual framework builtins with
# EXCLUDE_BUILTIN_MODS; the selection (and its loud failures) lives in
# psx_mod_catalog_select.cmake so the tests can drive it via `cmake -P`.
include("${CMAKE_CURRENT_LIST_DIR}/psx_mod_catalog_select.cmake")
# Immediate subdirectory names of `root` (package ids), sorted.
function(_psxrt_package_ids root out_var)
set(_ids "")
file(GLOB _entries CONFIGURE_DEPENDS LIST_DIRECTORIES true "${root}/*")
foreach(_e IN LISTS _entries)
if(IS_DIRECTORY "${_e}")
get_filename_component(_n "${_e}" NAME)
list(APPEND _ids "${_n}")
endif()
endforeach()
list(SORT _ids)
set(${out_var} "${_ids}" PARENT_SCOPE)
endfunction()
# Write `content` to `path` only when it differs, so re-configuring does not
# churn the mtime of a file the build depends on.
function(_psxrt_write_if_changed path content)
set(_prev "")
if(EXISTS "${path}")
file(READ "${path}" _prev)
endif()
if(NOT _prev STREQUAL "${content}")
file(WRITE "${path}" "${content}")
endif()
endfunction()
# Registered via cmake_language(DEFER) so it runs at the END of the directory
# that created the runtime targets. The staging targets already exist by then;
# this function adds sibling-aware validation targets after it has collected all
# targets sharing an output directory.
#
# Takes NO arguments and reads the pending targets out of global properties:
# cmake_language(DEFER CALL <fn> <arg>) does not carry a function-local
# variable through (measured with cmake 4.2.2 -- the callee receives an empty
# argument, and an add_custom_command built from it fails at directory end
# with "CMakeLists.txt:DEFERRED"), so nothing may be passed positionally here.
function(_psxrt_finalize_mod_catalog_guards)
get_property(_targets GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_TARGETS)
get_property(_manifests GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_MANIFESTS)
get_property(_dirs GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_DIRS)
get_property(_excludeds GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_EXCLUDED)
get_property(_stage_targets GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_STAGE_TARGETS)
list(LENGTH _targets _n)
if(_n EQUAL 0)
return()
endif()
math(EXPR _last "${_n} - 1")
# add_custom_command(TARGET) only accepts a target created in the current
# directory, and a deferred call runs in the directory that scheduled it,
# so a project with runtime targets in several directories gets one
# deferred pass per directory and each pass handles only its own.
set(_here_targets "")
set(_here_manifests "")
set(_here_excludeds "")
set(_here_stage_targets "")
foreach(_i RANGE 0 ${_last})
list(GET _dirs ${_i} _d)
if(_d STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
list(GET _targets ${_i} _t)
list(GET _manifests ${_i} _m)
list(GET _excludeds ${_i} _x)
list(GET _stage_targets ${_i} _s)
list(APPEND _here_targets "${_t}")
list(APPEND _here_manifests "${_m}")
list(APPEND _here_excludeds "${_x}")
list(APPEND _here_stage_targets "${_s}")
endif()
endforeach()
list(LENGTH _here_targets _n_here)
if(_n_here EQUAL 0)
return()
endif()
math(EXPR _last_here "${_n_here} - 1")
foreach(_i RANGE 0 ${_last_here})
list(GET _here_targets ${_i} _t)
list(GET _here_manifests ${_i} _own)
list(GET _here_excludeds ${_i} _own_excluded)
list(GET _here_stage_targets ${_i} _stage_target)
# Sibling runtime targets in this directory. Two of them can share one
# output directory (Tomba 2's US and Italian runtimes both land in the
# build root), in which case mods/bundled holds whichever linked last.
set(_alts "")
foreach(_j RANGE 0 ${_last_here})
if(NOT _j EQUAL _i)
list(GET _here_manifests ${_j} _m)
list(APPEND _alts "${_m}")
endif()
endforeach()
# "|" not ";": a semicolon inside an add_custom_command argument is a
# cmake list separator and would split the -D into two arguments.
list(JOIN _alts "|" _alts_joined)
set(_guard_target "${_t}_mod_catalog_check")
add_custom_target("${_guard_target}"
COMMAND ${CMAKE_COMMAND}
"-DPSX_MODS_DIR=$<TARGET_FILE_DIR:${_t}>/mods"
"-DPSX_CATALOG_MANIFEST=${_own}"
"-DPSX_CATALOG_EXCLUDED=${_own_excluded}"
"-DPSX_CATALOG_ALT_MANIFESTS=${_alts_joined}"
"-DPSX_REQUIRE_STAGED=1"
"-DPSX_LABEL=${_t}"
-P "${PSXRECOMP_ROOT}/runtime/psx_check_mod_catalog.cmake"
COMMENT "Verifying staged mod catalog for ${_t}"
VERBATIM)
add_dependencies("${_guard_target}" "${_stage_target}")
add_dependencies(${_t} "${_guard_target}")
# Siblings can share one output directory (above; and every
# PSX_PGXP_VARIANT clone sits beside its base). Each staging step
# wipes and rewrites mods/bundled, so under a parallel build one
# sibling's wipe raced the other's staging or check. Stage siblings
# one after another: each after the previous one staged and verified.
if(_i GREATER 0)
add_dependencies("${_stage_target}" "${_prev_guard_target}")
endif()
set(_prev_guard_target "${_guard_target}")
endforeach()
# One ctest, registered against the first staging target's output
# directory. REQUIRE_STAGED is 0 here: `ctest` may run in a tree where the
# runtime was never built, and a skip is more useful there than a spurious
# failure. The staging validation targets above pass 1, since they run
# immediately after staging where an absent catalog IS the defect.
get_property(_test_done GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_TEST_ADDED)
if(BUILD_TESTING AND NOT _test_done)
set_property(GLOBAL PROPERTY PSXRECOMP_MOD_CATALOG_TEST_ADDED TRUE)
list(GET _here_targets 0 _first)
list(GET _here_manifests 0 _first_manifest)
list(GET _here_excludeds 0 _first_excluded)
set(_first_alts "")
foreach(_j RANGE 0 ${_last_here})
if(NOT _j EQUAL 0)
list(GET _here_manifests ${_j} _m)
list(APPEND _first_alts "${_m}")
endif()
endforeach()
list(JOIN _first_alts "|" _first_alts_joined)
add_test(NAME psx_staged_mod_catalog_test
COMMAND ${CMAKE_COMMAND}
"-DPSX_MODS_DIR=$<TARGET_FILE_DIR:${_first}>/mods"
"-DPSX_CATALOG_MANIFEST=${_first_manifest}"
"-DPSX_CATALOG_EXCLUDED=${_first_excluded}"
"-DPSX_CATALOG_ALT_MANIFESTS=${_first_alts_joined}"
"-DPSX_REQUIRE_STAGED=0"
"-DPSX_LABEL=${_first}"
-P "${PSXRECOMP_ROOT}/runtime/psx_check_mod_catalog.cmake")
endif()
endfunction()
# Stage the framework's builtin packages and the title's own packages into
# <exe-dir>/mods/bundled, and register the guards described above.
#
# Trailing arguments are the target's EXCLUDE_BUILTIN_MODS: framework builtins
# this title declines to ship (runtime/psx_mod_catalog_select.cmake).
function(_psxrt_stage_mod_catalog target preloaded_dir)
set(_excluded_builtins ${ARGN})
# A build without local rewind (-DPSX_REWIND=OFF) has no ring for the
# built-in Rewind mod to arm, so the package is declined like any other
# excluded builtin: absent from the catalog and recorded as excluded for
# the release packager, rather than listed as a switch that does nothing.
# A title that overrides the id with its own package keeps it.
if(NOT PSXRECOMP_HAS_RBENGINE_SNAP AND
EXISTS "${PSXRECOMP_ROOT}/mods/builtin/packages/psx.feature.rewind" AND
NOT "psx.feature.rewind" IN_LIST _excluded_builtins AND
NOT (NOT preloaded_dir STREQUAL "" AND NOT preloaded_dir STREQUAL "NONE" AND
IS_DIRECTORY "${preloaded_dir}/packages/psx.feature.rewind"))
list(APPEND _excluded_builtins "psx.feature.rewind")
endif()
set(_out "$<TARGET_FILE_DIR:${target}>")
set(_ids "")
set(_copy "")
set(_builtin_copy "")
set(_game_copy "")
set(_game_ids "")
# ---- title-owned packages ---------------------------------------------
# Enumerated first so builtin selection can refuse an id the title both
# excludes and overrides; still COPIED after the builtins (see below).
set(_readme_copy "")
if(preloaded_dir STREQUAL "")
# The missed-title tripwire. A title whose source tree carries a mod
# catalog but does not declare it used to stage it itself into the
# wrong directory and find out at release time; now it cannot configure.
#
# Requires at least one package: the project scaffold creates an EMPTY
# mods/preloaded/packages (with a .gitkeep), and a title that has not
# written a mod yet must still configure.
set(_undeclared_ids "")
if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/mods/preloaded/packages")
_psxrt_package_ids(
"${CMAKE_SOURCE_DIR}/mods/preloaded/packages" _undeclared_ids)
endif()
if(_undeclared_ids)
list(JOIN _undeclared_ids "\n " _undeclared_pretty)
message(FATAL_ERROR
"This project has a mod catalog at "
"${CMAKE_SOURCE_DIR}/mods/preloaded/packages:\n"
" ${_undeclared_pretty}\n"
"but target '${target}' did not declare it, so those packages "
"would not be staged into <exe-dir>/mods/bundled and would not "
"appear on the Mods page or in a release.\n\n"
"Add it to the psxrecomp_add_runtime_target() call:\n\n"
" psxrecomp_add_runtime_target(${target}\n"
" ...\n"
" PRELOADED_MODS_DIR \"\${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded\"\n"
" )\n\n"
"and DELETE any add_custom_command(TARGET ${target} POST_BUILD "
"... copy_directory ... /mods) block: the framework now stages "
"both its own mods/builtin/packages and the title's packages, "
"so the layout lives in one place instead of six repositories. "
"Pass PRELOADED_MODS_DIR NONE if this target genuinely ships "
"no game catalog.\n\n"
"See docs/MOD_PACKAGES.md and bead beads-eio.3.101.")
endif()
elseif(NOT preloaded_dir STREQUAL "NONE")
if(NOT IS_DIRECTORY "${preloaded_dir}")
message(FATAL_ERROR
"PRELOADED_MODS_DIR for target '${target}' is not a directory: "
"${preloaded_dir}")
endif()
# An empty (or not-yet-created) packages/ subdirectory is the project
# scaffold's initial state and must still configure -- only a bad path
# is an error, because a wrong path is exactly how a title ends up
# shipping an empty Mods page.
if(IS_DIRECTORY "${preloaded_dir}/packages")
_psxrt_package_ids("${preloaded_dir}/packages" _game_ids)
endif()
if(NOT _game_ids)
message(STATUS
"psxrecomp: ${target} declares an empty mod catalog at "
"${preloaded_dir}/packages; staging framework packages only")
endif()
foreach(_id IN LISTS _game_ids)
list(APPEND _game_copy
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${preloaded_dir}/packages/${_id}"
"${_out}/mods/bundled/${_id}")
endforeach()
# mods/README.md used to arrive only as a side effect of copying the
# whole mods/preloaded tree into mods/. Now that only packages/ is
# staged, it has to be copied on purpose or it silently disappears.
if(EXISTS "${preloaded_dir}/README.md")
set(_readme_copy
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${preloaded_dir}/README.md"
"${_out}/mods/README.md")
endif()
endif()
# ---- framework-owned builtins (psx.*) ---------------------------------
# These target game_id "*" -- emulated-hardware features rather than
# per-disc content -- so every game gets them without carrying a copy of
# the manifests, minus any the title declines with EXCLUDE_BUILTIN_MODS.
set(_builtin_root "${PSXRECOMP_ROOT}/mods/builtin/packages")
set(_available_builtins "")
if(EXISTS "${_builtin_root}")
_psxrt_package_ids("${_builtin_root}" _available_builtins)
endif()
set(_allowlist "")
if(DEFINED PSX_BUILTIN_MOD_ALLOWLIST AND NOT
"${PSX_BUILTIN_MOD_ALLOWLIST}" STREQUAL "")
set(_allowlist ${PSX_BUILTIN_MOD_ALLOWLIST})
endif()
psx_select_builtin_mod_ids(_builtin_ids
LABEL "${target}"
AVAILABLE ${_available_builtins}
ALLOWLIST ${_allowlist}
EXCLUDE ${_excluded_builtins}
TITLE ${_game_ids})
if(_excluded_builtins)
set(_excluded_pretty ${_excluded_builtins})
list(REMOVE_DUPLICATES _excluded_pretty)
list(JOIN _excluded_pretty ", " _excluded_pretty)
message(STATUS
"psxrecomp: ${target} excludes framework builtin mod(s): "
"${_excluded_pretty}")
endif()
foreach(_id IN LISTS _builtin_ids)
list(APPEND _ids "${_id}")
list(APPEND _builtin_copy
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_builtin_root}/${_id}"
"${_out}/mods/bundled/${_id}")
endforeach()
list(APPEND _ids ${_game_ids})
# Framework first, title second: the title's copy of a shared id wins.
set(_copy ${_builtin_copy} ${_game_copy})
# The COPY commands above may legitimately name an id twice -- a title's
# catalog is allowed to OVERRIDE a framework builtin at the same id and
# version, and Tomba 2's Italian catalog does exactly that with localized
# psx.* manifests. Order carries that: the framework's copy runs first and
# the title's overwrites it. The id LIST, though, is an identity set used
# for the legacy purge, the staged-catalog assertion and the reported
# count, so it must be deduplicated or the build claims to stage 11
# packages when the catalog holds 7.
list(REMOVE_DUPLICATES _ids)
if(NOT _ids)
return()
endif()
# Removing the ids THIS build stages from any pre-existing mods/packages
# migrates a build directory made before the bundled/ split, so the
# build-time guard's "an id turned up in mods/packages" report can only
# mean a stray copy made by this build. Ids the build does not stage are
# deliberately left alone: on a self-compiling setup release they can be
# packages the player installed under the old layout, and the runtime's
# migrate_legacy_root() relocates those into mods/installed on next launch.
set(_purge "")
foreach(_id IN LISTS _ids)
list(APPEND _purge "${_out}/mods/packages/${_id}")
endforeach()
# The id list the build-time guard and the ctest assert against.
set(_manifest "${CMAKE_CURRENT_BINARY_DIR}/psx_mod_catalog_${target}.txt")
list(SORT _ids)
string(JOIN "\n" _manifest_text ${_ids})
_psxrt_write_if_changed("${_manifest}" "${_manifest_text}\n")
# ...and the builtins this target declined, which must be ABSENT. Written
# even when empty so a packager reading it (tools/release_stage.py) never
# has to guess whether "no file" means "nothing excluded" or "old build".
set(_excluded_manifest
"${CMAKE_CURRENT_BINARY_DIR}/psx_mod_catalog_${target}.excluded.txt")
set(_excluded_sorted ${_excluded_builtins})
list(REMOVE_DUPLICATES _excluded_sorted)
list(SORT _excluded_sorted)
set(_excluded_text "# framework builtin mod packages ${target} excludes (EXCLUDE_BUILTIN_MODS)\n")
foreach(_id IN LISTS _excluded_sorted)
string(APPEND _excluded_text "${_id}\n")
endforeach()
_psxrt_write_if_changed("${_excluded_manifest}" "${_excluded_text}")
# A custom target, rather than LINK_DEPENDS + POST_BUILD, is portable to
# Ninja, Make, Visual Studio, and Xcode. It is intentionally always
# out-of-date: the catalog is small, and that makes added/removed package
# assets correct without forcing CMake to reconfigure or the game to link.
# The target is wired as a prerequisite below, so building the game always
# stages the catalog first while leaving its code objects untouched.
set(_stage_target "${target}_mod_catalog_stage")
list(LENGTH _ids _n_ids)
add_custom_target("${_stage_target}"
# Wipe first: copy_directory MERGES, so a package deleted from source
# would otherwise survive in the build output forever and keep
# appearing on the Mods page (and inflate release catalog assertions).
#
# Scoped to mods/bundled, which is build output and nothing else.
# mods/installed/ is the launcher's (player-installed .psxmod archives)
# and mods/state.toml is user selection state; a build must never touch
# either. Before the split this wipe was scoped to mods/packages, which
# ALSO held everything the player had installed -- so a rebuild,
# including the one a self-compiling setup release runs on the player's
# own machine, deleted their mods without a word.
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_out}/mods/bundled"
COMMAND ${CMAKE_COMMAND} -E rm -rf ${_purge}
${_copy}
${_readme_copy}
COMMENT "Staging mod catalog for ${target} (${_n_ids} package(s) -> mods/bundled)"
VERBATIM)
# Every trusted plugin the executable registers must be selectable by an
# authored manifest [[plugin]] id; the resolved plan activates plugins by
# manifest id only, so an undeclared id (typically a hook registered under
# a sub-id of its package's plugin) is dead code that fails silently.
# Audits the AUTHORED trees, so developer packages a release catalog
# strips still count as declarations.
if(BUILD_TESTING)
set(_audit_roots "${PSXRECOMP_ROOT}/mods/builtin")
if(NOT preloaded_dir STREQUAL "" AND NOT preloaded_dir STREQUAL "NONE"
AND IS_DIRECTORY "${preloaded_dir}")
list(APPEND _audit_roots "${preloaded_dir}")
endif()
get_target_property(_extra_roots ${target} PSXRT_AUDIT_MOD_DIRS)
if(_extra_roots)
list(APPEND _audit_roots ${_extra_roots})
endif()
add_test(NAME psx_mod_plugin_audit_${target}
COMMAND $<TARGET_FILE:${target}> --audit-mod-plugins ${_audit_roots})
endif()
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_TARGETS "${target}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_MANIFESTS "${_manifest}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_EXCLUDED "${_excluded_manifest}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_STAGE_TARGETS "${_stage_target}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_DIRS
"${CMAKE_CURRENT_SOURCE_DIR}")
# Schedule the guard pass once per directory, not once per target.
get_property(_scheduled DIRECTORY PROPERTY PSXRECOMP_MOD_CATALOG_DEFERRED)
if(NOT _scheduled)
set_property(DIRECTORY PROPERTY PSXRECOMP_MOD_CATALOG_DEFERRED TRUE)
cmake_language(DEFER CALL _psxrt_finalize_mod_catalog_guards)
endif()
endfunction()
# Memory-card slots a title uses (MEMCARD_SLOTS, default 2). 1 for games that
# only talk to port 1: the launcher hides slot 2 and the runtime leaves it empty.
function(psxrt_resolve_memcard_slots out_var value)
if(value STREQUAL "")
set(value 2)
endif()
if(NOT "${value}" MATCHES "^[12]$")
message(FATAL_ERROR "MEMCARD_SLOTS must be 1 or 2 (got ${value})")
endif()
set(${out_var} ${value} PARENT_SCOPE)
endfunction()
function(psxrecomp_add_runtime_target target)
# 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; docs/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.
# PGXP_CLONE is internal: set only by the PSX_PGXP_VARIANT auto-clone at the
# end of this function, to mark the sibling that needs a distinguishing exe
# name. Callers pass PGXP alone.
set(options ORACLE COSIM PGXP PGXP_CLONE)
set(oneValueArgs
GAME_GENERATED_DISPATCH_C
GAME_OVERLAY_STATIC_C
BIOS_GENERATED_FULL_C
BIOS_GENERATED_DISPATCH_C
DEBUG_PORT
WINDOW_TITLE
DEFAULT_BIOS_PATH
DEFAULT_GAME_CONFIG_PATH
LAUNCHER_BOXART
LAUNCHER_PAD
LAUNCHER_BRAND
EXE_NAME
GAME_VERSION
MAX_PLAYERS
MEMCARD_SLOTS
APP_ICON
# The title's own mod catalog source directory -- the one shaped like
# <repo>/mods/preloaded, holding packages/<id>/<version>/manifest.toml
# and an optional README.md. The FRAMEWORK stages it, together with its
# own mods/builtin/packages, into <exe-dir>/mods/bundled. Titles must
# not stage it themselves; see _psxrt_stage_mod_catalog below for why
# that is now a build error rather than a convention. Pass the literal
# NONE to declare, explicitly, that this target ships no game catalog.
PRELOADED_MODS_DIR
)
# GAME_GENERATED_FULL_C is a list (not a single value): the split-TU build
# writes the recompiled game as N full_NN.c shards instead of one
# monolithic full.c, so this argument may carry 1..N paths. A single path
# is just a one-element list, so games still passing one file are
# unaffected.
#
# EXCLUDE_BUILTIN_MODS names framework builtin packages (ids under
# mods/builtin/packages) this target does not ship. They are left out of
# <exe-dir>/mods/bundled entirely -- absent, not hidden -- so no packager
# can ship them either. An id that is not a builtin fails configure.
#
# AUDIT_MOD_DIRS names further authored package trees (manifest.toml files
# are found recursively) that declare plugins this executable registers but
# no bundled catalog ships, e.g. development-only packages. They are
# audited, never staged.
set(multiValueArgs EXTRAS_SOURCES GAME_GENERATED_FULL_C EXCLUDE_BUILTIN_MODS
AUDIT_MOD_DIRS)
cmake_parse_arguments(PSXRT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# DEBUG_PORT and WINDOW_TITLE were previously required cmake-time defaults.
# game.toml's [runtime] block is now the source of truth at run time; the
# cmake-time values only survive as fallback when --game is not passed.
if(NOT PSXRT_DEBUG_PORT)
set(PSXRT_DEBUG_PORT 4370)
endif()
if(NOT PSXRT_WINDOW_TITLE)
set(PSXRT_WINDOW_TITLE "${target}")
endif()
# The baked default BIOS path must never be absolute: an absolute path is a
# build-machine path, and a promoted/release exe carrying it will silently
# load the BUILDER'S BIOS wherever that path exists (i.e. on the dev box) —
# so the "prompts for a BIOS on a clean install" flow is never exercised
# where releases are validated. Dev checkouts still resolve the relative
# default without prompting via the exe-dir upward search, which also tries
# <ancestor>/psxrecomp-v4/<relative> for game-project layouts.
# Follow the stem this build actually pins; assuming SCPH1001 here handed
# every non-SCPH1001 kit a default path that could never resolve.
set(_psxrt_stem_bios "bios/${PSXRECOMP_BIOS_STEM}.BIN")
if(NOT PSXRT_DEFAULT_BIOS_PATH)
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
elseif(IS_ABSOLUTE "${PSXRT_DEFAULT_BIOS_PATH}")
message(WARNING
"DEFAULT_BIOS_PATH '${PSXRT_DEFAULT_BIOS_PATH}' is absolute; refusing to "
"bake a build-machine path into the binary (release exes must prompt on "
"user machines). Using relative '${_psxrt_stem_bios}' instead — drop the "
"DEFAULT_BIOS_PATH argument from this game's CMakeLists.")
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
endif()
if(NOT DEFINED PSXRT_DEFAULT_GAME_CONFIG_PATH)
set(PSXRT_DEFAULT_GAME_CONFIG_PATH "")
endif()
if(PSXRT_BIOS_GENERATED_FULL_C AND PSXRT_BIOS_GENERATED_DISPATCH_C)
# Per-game BIOS pin: the pinned files REPLACE the matching stem's
# fork-global generated files only. Every other backend and the
# generated psx_bios_registry.c stay linked — replacing the whole set
# (the previous behaviour) dropped the registry and the other
# backends, producing undefined references to psx_bios_registry at
# link time. The registry's extern for the pinned stem is satisfied
# by the pinned dispatch.c, which carries the same backend descriptor.
get_filename_component(_psxrt_pin_name "${PSXRT_BIOS_GENERATED_DISPATCH_C}" NAME)
string(REPLACE "_dispatch.c" "" _psxrt_pin_stem "${_psxrt_pin_name}")
set(generated_sources "")
foreach(_psxrt_src IN LISTS PSXRECOMP_BIOS_GENERATED)
get_filename_component(_psxrt_src_name "${_psxrt_src}" NAME)
if(_psxrt_src_name STREQUAL "${_psxrt_pin_stem}_full.c" OR
_psxrt_src_name STREQUAL "${_psxrt_pin_stem}_dispatch.c")
# replaced by the game's pinned copy
else()
list(APPEND generated_sources "${_psxrt_src}")
endif()
endforeach()
list(APPEND generated_sources
"${PSXRT_BIOS_GENERATED_FULL_C}"
"${PSXRT_BIOS_GENERATED_DISPATCH_C}")
set_source_files_properties(${generated_sources} PROPERTIES GENERATED TRUE)
message(STATUS "psxrecomp: BIOS stem ${_psxrt_pin_stem} pinned to game-local copies")
else()
set(generated_sources ${PSXRECOMP_BIOS_GENERATED})
endif()
# Game recompiled C that a from-source builder must generate before building
# (see the require-generated guard added after add_executable below). Collected
# here so the guard names the exact files that are missing.
set(_game_generated_check "")
if(PSXRT_GAME_GENERATED_FULL_C)
foreach(_full_src IN LISTS PSXRT_GAME_GENERATED_FULL_C)
set_source_files_properties("${_full_src}" PROPERTIES GENERATED TRUE)
list(APPEND generated_sources "${_full_src}")
list(APPEND _game_generated_check "${_full_src}")
endforeach()
set(has_game_dispatch TRUE)
endif()
if(PSXRT_GAME_GENERATED_DISPATCH_C)
set_source_files_properties("${PSXRT_GAME_GENERATED_DISPATCH_C}" PROPERTIES GENERATED TRUE)
list(APPEND generated_sources "${PSXRT_GAME_GENERATED_DISPATCH_C}")
list(APPEND _game_generated_check "${PSXRT_GAME_GENERATED_DISPATCH_C}")
set(has_game_dispatch TRUE)
if(EXISTS "${PSXRT_GAME_GENERATED_DISPATCH_C}")
file(STRINGS "${PSXRT_GAME_GENERATED_DISPATCH_C}"
game_dispatch_native_ok_decl
REGEX "int[ \t]+psx_game_text_native_ok\\("
LIMIT_COUNT 1)
if(game_dispatch_native_ok_decl)
set(has_game_dispatch_native_ok TRUE)
endif()
file(STRINGS "${PSXRT_GAME_GENERATED_DISPATCH_C}"
game_dispatch_native_ok_full_decl
REGEX "int[ \t]+psx_game_text_native_ok_full\\("
LIMIT_COUNT 1)
if(game_dispatch_native_ok_full_decl)
set(has_game_dispatch_native_ok_full TRUE)
endif()
endif()
endif()
# Layer B: statically-compiled overlay dispatch, generated from the
# player's disc by psxrecomp_cli.py generate (tools/aot_overlay_pipeline.py
# `static` for a profile that declares static_output, or
# compile_overlays.py --static; docs/AOT_SHARDING.md). Absent is legal --
# CI never has game bytes -- but it is reported, never silent, and a
# profile/CMake disagreement on the file is a configure error.
# overlays_static.c is the dispatcher; each overlay is its own
# overlays_static_NNNN.c unit beside it (one 300 MB file compiled on one
# core is what that replaced). Both are globbed with CONFIGURE_DEPENDS, so
# a Generate after this configure re-runs it at build time.
if(has_game_dispatch)
set(_ov_static_game_linked TRUE)
else()
set(_ov_static_game_linked FALSE)
endif()
psxrecomp_overlay_static_sources(_ov_static_sources _ov_static_present
TARGET "${target}"
STATIC_C "${PSXRT_GAME_OVERLAY_STATIC_C}"
PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/aot/overlays.json"
GAME_LINKED ${_ov_static_game_linked})
if(_ov_static_present)
set_source_files_properties(${_ov_static_sources} PROPERTIES GENERATED TRUE)
list(APPEND generated_sources ${_ov_static_sources})
set(has_overlay_dispatch TRUE)
endif()
if(PSXRT_ORACLE)
set(mode_source ${PSXRECOMP_ROOT}/runtime/src/psx_interpreter.c)
else()
set(mode_source ${PSXRECOMP_ROOT}/runtime/src/stub_interpreter.c)
endif()
add_executable(${target}
${PSXRECOMP_RUNTIME_SOURCES}
${mode_source}
${generated_sources}
${PSXRT_EXTRAS_SOURCES}
)
target_link_libraries(${target} PRIVATE chdr-static psx_texture_image_decode)
psxrecomp_windows_manifest(${target})
psxrecomp_sha256_file(${target})
set(_execution_contract
"${PSXRECOMP_ROOT}/runtime/src/gte.cpp"
"${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp"
"${PSXRECOMP_ROOT}/runtime/include/hle_gte.h"
"${PSXRECOMP_ROOT}/runtime/include/cpu_state.h"
"${PSXRECOMP_ROOT}/runtime/include/gte.h"
"${PSXRECOMP_ROOT}/runtime/include/pgxp.h"
"${PSXRECOMP_ROOT}/runtime/include/render_pass_projection.h")
if(PSXRT_ORACLE)
psxrecomp_execution_profile(${target} PROFILE REFERENCE CONTRACT_FILES ${_execution_contract})
else()
psxrecomp_execution_profile(${target} CONTRACT_FILES ${_execution_contract})
endif()
# audio_trace.c uses C11 atomics. Make the runtime's actual language
# requirement explicit instead of relying on a parent project's global
# CMAKE_C_STANDARD setting. cxx_std_17 likewise — game CMakeLists may omit
# CMAKE_CXX_STANDARD; mod_packages.cpp must not compile as a pre-17 dialect.
target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
psxrecomp_apply_runtime_ipo(${target})
if(NOT PSX_PGO STREQUAL "")
if(NOT PSX_PGO STREQUAL "generate" AND NOT PSX_PGO STREQUAL "use")
message(FATAL_ERROR
"PSX_PGO must be empty, 'generate', or 'use' (got '${PSX_PGO}')")
endif()
set(_psx_pgo_dir "${CMAKE_BINARY_DIR}/pgo")
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
if(PSX_PGO STREQUAL "generate")
target_compile_options(${target} PRIVATE -fprofile-instr-generate)
target_link_options(${target} PRIVATE -fprofile-instr-generate)
else()
set(_psx_pgo_profile "${_psx_pgo_dir}/default.profdata")
target_compile_options(${target} PRIVATE
"-fprofile-instr-use=${_psx_pgo_profile}"
-Wno-profile-instr-out-of-date
-Wno-profile-instr-unprofiled)
target_link_options(${target} PRIVATE
"-fprofile-instr-use=${_psx_pgo_profile}")
endif()
elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU")
# GCC writes .gcda files directly; run_pgo_train already recognizes
# those as a completed training run, so no llvm-profdata merge is
# required for this branch.
if(PSX_PGO STREQUAL "generate")
target_compile_options(${target} PRIVATE
"-fprofile-generate=${_psx_pgo_dir}")
target_link_options(${target} PRIVATE
"-fprofile-generate=${_psx_pgo_dir}")
else()
target_compile_options(${target} PRIVATE
"-fprofile-use=${_psx_pgo_dir}"
-Wno-missing-profile)
target_link_options(${target} PRIVATE
"-fprofile-use=${_psx_pgo_dir}")
endif()
else()
message(FATAL_ERROR
"PSX_PGO requires a Clang or GNU compiler (got ${CMAKE_C_COMPILER_ID})")
endif()
unset(_psx_pgo_dir)
unset(_psx_pgo_profile)
endif()
# Game-specific executable name. Every title instantiates this function with
# the same CMake target name ("psx-runtime"), so without this they ALL produce
# an identical "psx-runtime.exe" — launching or killing one title's process by
# name then hits another title's running instance (e.g. an X5 dev run killing a
# concurrent Tomba 2 run in a sibling worktree). An explicit EXE_NAME wins;
# otherwise derive a unique, filename-safe OUTPUT_NAME from the window title
# (which is already per-game) so each title's binary is distinct
# (MegaManX5Recomp.exe, Tomba2Recomp.exe, ...). The CMake target name stays
# "psx-runtime", so $<TARGET_FILE...> references and the POST_BUILD asset
# copies below are unaffected. Oracle builds get an _oracle suffix so a game
# and its Beetle oracle don't collide either.
if(PSXRT_EXE_NAME)
set(_psxrt_exe_name "${PSXRT_EXE_NAME}")
else()
string(MAKE_C_IDENTIFIER "${PSXRT_WINDOW_TITLE}" _psxrt_exe_name)
endif()
if(PSXRT_ORACLE)
set(_psxrt_exe_name "${_psxrt_exe_name}_oracle")
endif()
if(PSXRT_PGXP)
# The _pgxp suffix exists only to keep the auto-cloned A/B sibling
# distinct from the base binary it sits beside (PSX_PGXP_VARIANT); the
# launcher or the player picks between them. Same debug port as the
# base build — run one at a time (the A/B protocol is
# one-toggle-per-run anyway).
#
# A title that builds PGXP into its ONE runtime target has no base
# binary beside it, so suffixing there would ship a single product
# under an odd name and, worse, leave a second look-alike executable in
# the build tree for someone to launch by mistake. That is not
# hypothetical: a non-PGXP binary got played and reported as "textures
# still wobble" while the PGXP one measured 99% precise-vertex
# coverage. Suffix the clone, never the primary.
if(PSXRT_PGXP_CLONE)
set(_psxrt_exe_name "${_psxrt_exe_name}_pgxp")
endif()
target_compile_definitions(${target} PRIVATE
PSX_PGXP=1
PSX_OVERLAY_FLAVOR=2) # PSX_OVERLAY_FLAVOR_PGXP (overlay_api.h)
set(_psxrt_overlay_flavor 2)
else()
set(_psxrt_overlay_flavor 0)
endif()
set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}")
# Publish the OVERLAY CODEGEN FLAVOR this target links with, for the same
# reason the exe name is published just below: so nothing downstream has to
# re-derive it.
#
# The flavor is the high half of overlay_abi() and the `_f<n>` field of the
# shard cache tag (overlay_api.h PSX_OVERLAY_FLAVOR; 0 base, 2 pgxp). It is
# a property of the BINARY, decided right here, and it is NOT
# platform-dependent — a Windows and a Linux build of the same target share
# it. Every release packager needs it to name the cache namespace the
# shipped runtime will actually read, and until now every packager simply
# assumed 0. That assumption is invisible when it is wrong: a PGXP runtime
# reads cache/<id>/gcc/<arch-abi>/cg..._f2/ while the packager stages
# ..._f0/, so the package ships a cache the binary ignores completely and
# every overlay runs interpreted, with nothing failing anywhere.
#
# tools/release_stage.py reads this file (cg-tag --flavor-from-build), so a
# packager can stop guessing. See bead beads-eio.3.102.
file(GENERATE
OUTPUT "${CMAKE_BINARY_DIR}/psxrecomp_overlay_flavor-${target}.txt"
CONTENT "${_psxrt_overlay_flavor}\n")
# Publish the name we just chose, so nothing downstream has to re-derive it.
#
# Two other places used to compute this independently — psxrecomp_cli.py
# from --exe-name, and the in-runtime self-compiler from
# codegen_setup.exe_basename — each re-running the same
# MAKE_C_IDENTIFIER(WINDOW_TITLE) rule against its own copy of the title.
# The rules agree; the copies drift. Rename a game and the build links
# <new>.exe while both consumers look for <old>.exe, which surfaces as the
# flatly untrue "build succeeded but binary missing" on a build that had no
# errors at all. Seen in the wild on Revelations: Persona, where CMake
# emitted Revelations__Persona_Recompiled.exe and the setup host wanted
# Revelations_Persona__Recompiled.exe — same algorithm, colon in a
# different place.
#
# Generate-time output uses the final target property, so a game that
# changes OUTPUT_NAME after this helper returns still publishes the name
# CMake will actually link.
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/psxrecomp_exe_name-${target}.txt"
CONTENT "$<TARGET_FILE_BASE_NAME:${target}>\n")
# ---- Windows / desktop app icon ---------------------------------------
# Prefer an explicit APP_ICON, then the game-repo copy under assets/, then
# the framework default shipped in psxrecomp/assets (Retro-themed pad).
if(NOT PSXRT_APP_ICON)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico")
set(PSXRT_APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico")
elseif(EXISTS "${PSXRECOMP_ROOT}/assets/psxrecomp.ico")
set(PSXRT_APP_ICON "${PSXRECOMP_ROOT}/assets/psxrecomp.ico")
endif()
endif()
if(PSXRT_APP_ICON AND EXISTS "${PSXRT_APP_ICON}")
if(WIN32)
# clang/llvm-mingw CI needs an RC compiler or the .rc is ignored and
# the PE ships without an embedded icon.
enable_language(RC)
if(NOT CMAKE_RC_COMPILER)
find_program(CMAKE_RC_COMPILER
NAMES llvm-rc llvm-windres windres
HINTS
"$ENV{RETCOMM_TOOLCHAIN}/bin"
"$ENV{CMAKE_CLANG_V1}/bin"
DOC "Windows resource compiler for APP_ICON .rc")
endif()
if(CMAKE_RC_COMPILER)
string(REPLACE "\\" "/" _psxrt_ico_fwd "${PSXRT_APP_ICON}")
set(_psxrt_rc "${CMAKE_CURRENT_BINARY_DIR}/${target}_app_icon.rc")
file(WRITE "${_psxrt_rc}" "IDI_ICON1 ICON \"${_psxrt_ico_fwd}\"\n")
# The icon has no dependency on runtime macros or headers.
# Compile it separately: windres launches a preprocessor through
# a shell, which cannot round-trip the executable's quoted
# string definitions (notably the pipe-separated BIOS stems).
add_library(${target}_app_icon OBJECT "${_psxrt_rc}")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:${target}_app_icon>)
message(STATUS "psxrecomp ${target}: APP_ICON=${PSXRT_APP_ICON} (RC=${CMAKE_RC_COMPILER})")
else()
message(WARNING
"psxrecomp ${target}: APP_ICON set but no RC compiler "
"(llvm-rc/windres) — PE will have no embedded icon; "
"runtime still loads assets/psxrecomp.png via SDL")
endif()
else()
message(STATUS "psxrecomp ${target}: APP_ICON=${PSXRT_APP_ICON} (window icon via PNG)")
endif()
# Stage PNG beside the exe when present (AppImage / desktop / SDL icon).
get_filename_component(_psxrt_ico_dir "${PSXRT_APP_ICON}" DIRECTORY)
set(_psxrt_png "${_psxrt_ico_dir}/psxrecomp.png")
if(EXISTS "${_psxrt_png}")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:${target}>/assets"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_psxrt_png}"
"$<TARGET_FILE_DIR:${target}>/assets/psxrecomp.png"
COMMENT "Staging psxrecomp.png app icon"
VERBATIM)
endif()
endif()
# ---- overlay codegen hash (auto cache key) -----------------------------
# Hash the recompiler's codegen sources into runtime/include/overlay_codegen_hash.h
# (gitignored) so the overlay cache path carries cg<N>_<hash>: any emitter change
# auto-invalidates the cache instead of silently reusing a stale-but-cgN DLL (the
# v0.3.0 black-screen). The loader (via overlay_api.h) and compile_overlays.py both
# read the same generated PSX_OVERLAY_CODEGEN_HASH, so they never drift. Defined
# once (shared across psx-runtime/psx-beetle); idempotent write avoids rebuilds.
set(_codegen_hash_hdr ${PSXRECOMP_ROOT}/runtime/include/overlay_codegen_hash.h)
if(NOT TARGET psxrecomp_codegen_hash)
# Canonical source list shared with recompiler/CMakeLists.txt (which bakes
# the SAME hash into psxrecomp-game for the --codegen-hash staleness guard).
set(PSXRECOMP_CODEGEN_HASH_ROOT ${PSXRECOMP_ROOT})
include(${PSXRECOMP_ROOT}/runtime/codegen_hash_sources.cmake)
set(_codegen_srcs ${PSXRECOMP_CODEGEN_HASH_SRCS})
add_custom_command(
OUTPUT ${_codegen_hash_hdr}
COMMAND ${CMAKE_COMMAND} -DOUT=${_codegen_hash_hdr} "-DSRCS=${_codegen_srcs}"
-P ${PSXRECOMP_ROOT}/runtime/hash_codegen.cmake
DEPENDS ${_codegen_srcs} ${PSXRECOMP_ROOT}/runtime/hash_codegen.cmake
COMMENT "Hashing recompiler codegen -> overlay_codegen_hash.h"
VERBATIM)
add_custom_target(psxrecomp_codegen_hash DEPENDS ${_codegen_hash_hdr})
endif()
add_dependencies(${target} psxrecomp_codegen_hash)
# ---- require-generated guard -------------------------------------------
# The game's recompiled C (generated/<serial>_{full,dispatch}.c) is produced
# by the recompiler tool in a step BEFORE this build, and its paths are marked
# GENERATED so `cmake configure` succeeds before that step has run. Without a
# guard, a builder who skips generation only finds out deep in the build via
# cc1: fatal error: .../<serial>_full.c: No such file or directory
# with no hint that a step was skipped or what produces the file. Catch it
# first — a WARNING now (early, at configure) and a hard, actionable stop at
# build start (below) — so the raw compiler error is never the first signal.
# Only guards GAME sources: the BIOS path is either bundled OpenBIOS (emitted
# by a custom command here) or has its own staleness check above.
if(_game_generated_check)
if(EXISTS "${PSXRECOMP_ROOT}/recompiler/build/psxrecomp-game.exe")
set(_psxrt_recompiler_hint "${PSXRECOMP_ROOT}/recompiler/build/psxrecomp-game.exe")
else()
set(_psxrt_recompiler_hint "${PSXRECOMP_ROOT}/recompiler/build/psxrecomp-game")
endif()
set(_psxrt_missing_now "")
foreach(_g IN LISTS _game_generated_check)
if(NOT EXISTS "${_g}")
list(APPEND _psxrt_missing_now "${_g}")
endif()
endforeach()
if(_psxrt_missing_now)
message(WARNING
"${target}: recompiled game C is not present yet — the build will "
"fail until you generate it.\n"
" Run the recompiler once: ${_psxrt_recompiler_hint} --config "
"${PSXRT_DEFAULT_GAME_CONFIG_PATH}\n"
" (build that tool first if needed; see psxrecomp/docs/BUILDING.md). "
"This is expected on a fresh checkout before the first generation.")
endif()
# Pass paths via a list file — large shard counts (hundreds of
# generated/*_full_*.c) make -DSOURCES=... exceed Windows' ~8191-char
# CreateProcess limit ("The system cannot execute the specified program").
set(_psxrt_gen_list
"${CMAKE_CURRENT_BINARY_DIR}/${target}_generated_sources.txt")
file(WRITE "${_psxrt_gen_list}" "")
foreach(_g IN LISTS _game_generated_check)
file(APPEND "${_psxrt_gen_list}" "${_g}\n")
endforeach()
add_custom_target(${target}_require_generated
COMMAND ${CMAKE_COMMAND}
"-DSOURCES_FILE=${_psxrt_gen_list}"
"-DTARGET=${target}"
"-DGAME_CONFIG=${PSXRT_DEFAULT_GAME_CONFIG_PATH}"
"-DRECOMPILER=${_psxrt_recompiler_hint}"
"-DDOC=psxrecomp/docs/BUILDING.md (\"Build and run a game\")"
-P "${PSXRECOMP_ROOT}/runtime/check_generated_sources.cmake"
COMMENT "Verifying recompiled game C exists for ${target}"
VERBATIM)
# Target-level dependency: this check runs to completion before ANY of
# ${target}'s objects compile, so a missing generated source aborts with
# our message rather than the compiler's.
add_dependencies(${target} ${target}_require_generated)
endif()
# Force the cg-tag CONSUMERS to recompile whenever overlay_codegen_hash.h
# changes. overlay_api.h pulls that header via __has_include, which the
# compiler depfile does NOT record when the header is absent at first compile —
# so a later hash change left a STALE baked-in PSX_OVERLAY_CODEGEN_HASH in the
# binary, making the LOADER read cg<old> while autocompile WROTE cg<new> (the
# read≠write overlay lag/wedge class — the runtime silently ignored the freshly
# compiled shards). An explicit OBJECT_DEPENDS makes the dependency
# unconditional, so the runtime's cg tag can never drift from the headers /
# autocompile again. (add_dependencies above only orders header generation; it
# does not force object recompiles on content change.)
set_source_files_properties(
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
PROPERTIES OBJECT_DEPENDS ${_codegen_hash_hdr})
target_include_directories(${target} PRIVATE
${PSXRECOMP_RUNTIME_INCLUDE_DIRS}
${PSX_SDL_INCLUDE_DIRS}
)
# pkg-config reports fallback SDL2_LIBRARIES as a bare name
# (e.g. "SDL2" -> -lSDL2);
# add its library dirs so the linker finds it outside default paths
# (e.g. Homebrew's /opt/homebrew/lib on macOS). Empty/harmless on MSVC.
if(PSX_SDL_LIBRARY_DIRS)
target_link_directories(${target} PRIVATE ${PSX_SDL_LIBRARY_DIRS})
endif()
# For a self-contained MinGW SDL2 fallback build, link SDL2 statically via
# pkg-config's
# --static link line (libSDL2.a + the full Windows system-lib chain SDL2
# needs: winmm, imm32, ole32, oleaut32, version, setupapi, dinput8, ...).
# Otherwise link the SDL2 import lib (needs SDL2.dll at runtime).
if(PSX_STATIC_RUNTIME AND PSX_SDL_STATIC_LDFLAGS)
target_link_libraries(${target} PRIVATE ${PSX_SDL_STATIC_LDFLAGS})
else()
target_link_libraries(${target} PRIVATE ${PSX_SDL_LIBRARIES})
endif()
# zlib: boot_state v4 savestate compression (RAM/VRAM/SPU blobs).
# Portable Windows toolchains (cmake-clang-v1) have no system zlib —
# fetch a pinned release when find_package fails (same pattern as SDL3).
psxrecomp_ensure_zlib()
target_link_libraries(${target} PRIVATE ZLIB::ZLIB)
# Build identity: stamp the psxrecomp commit into the binary so a crash report
# can be correlated to an exact build (issue #1 user reports had no version).
# Computed at configure time from the psxrecomp repo (this file's dir); empty
# on failure (no git / not a repo) -> crash_trace.c falls back to "unknown".
psxrecomp_build_revision(PSX_GIT_REV "${CMAKE_CURRENT_FUNCTION_LIST_DIR}")
# Release pin for lobby matching (create/join/list). Override via
# GAME_VERSION arg or -DPSX_GAME_VERSION=...; default "dev".
if(NOT PSXRT_GAME_VERSION)
if(DEFINED PSX_GAME_VERSION AND NOT PSX_GAME_VERSION STREQUAL "")
set(PSXRT_GAME_VERSION "${PSX_GAME_VERSION}")
else()
set(PSXRT_GAME_VERSION "dev")
endif()
endif()
# Per-game netplay/local pad ceiling. Default 2 (MotK / dual-shock path).
# Single-player titles (Tomba, Ape Escape, …) pass MAX_PLAYERS 1 so rewind
# / rbengine still link without advertising multiplayer. Multitap N-player
# (Bomberman Party Edition) uses 5; dual SCPH-1070 uses 8. Range matches
# sio.h (1..8).
if(NOT PSXRT_MAX_PLAYERS)
if(DEFINED PSX_MAX_PLAYERS AND NOT PSX_MAX_PLAYERS STREQUAL "")
set(PSXRT_MAX_PLAYERS "${PSX_MAX_PLAYERS}")
else()
set(PSXRT_MAX_PLAYERS 2)
endif()
endif()
if(PSXRT_MAX_PLAYERS LESS 1 OR PSXRT_MAX_PLAYERS GREATER 8)
message(FATAL_ERROR
"MAX_PLAYERS must be in 1..8 (got ${PSXRT_MAX_PLAYERS})")
endif()
message(STATUS "psxrecomp ${target}: PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS}")
psxrt_resolve_memcard_slots(PSXRT_MEMCARD_SLOTS "${PSXRT_MEMCARD_SLOTS}")
target_compile_definitions(${target} PRIVATE
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}"
# The retail stem this build pins. A setup host has no linked
# backend to ask, so this is how it knows which image to look for
# and name (psx_bios_known_images.h) instead of assuming SCPH-1001.
PSX_EXPECTED_BIOS_STEM="${PSXRECOMP_EXPECTED_RETAIL_STEM}"
PSX_SETUP_BIOS_STEMS="${PSXRECOMP_SETUP_BIOS_STEMS}"
PSX_SETUP_FRAMEWORK_REL="${PSXRECOMP_SETUP_FRAMEWORK_REL}"
# Where the shipped redistributable image lives, relative to the exe.
# This is what a player gets when they choose no BIOS.
PSX_BUNDLED_BIOS_PATH="${PSXRECOMP_BUNDLED_BIOS_PATH}"
PSX_DEFAULT_GAME_CONFIG_PATH="${PSXRT_DEFAULT_GAME_CONFIG_PATH}"
PSX_WINDOW_TITLE="${PSXRT_WINDOW_TITLE}"
PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS}
PSX_MEMCARD_SLOTS=${PSXRT_MEMCARD_SLOTS}
FMT_HEADER_ONLY=1
$<$<PLATFORM_ID:Windows>:NOMINMAX>
$<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>
$<$<CXX_COMPILER_ID:MSVC>:SDL_MAIN_HANDLED>
)
# Version / git rev change often on package updates. Keep them off the
# target-wide compile line so Ninja does not rebuild every runtime + shard TU.
set_source_files_properties(
"${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.c"
PROPERTIES COMPILE_DEFINITIONS
"PSX_GAME_VERSION=\"${PSXRT_GAME_VERSION}\";$<$<BOOL:${PSX_LOBBY_DEV_LIST}>:PSX_LOBBY_DEV_LIST=1>"
)
set_source_files_properties(
"${PSXRECOMP_ROOT}/runtime/src/crash_trace.c"
PROPERTIES COMPILE_DEFINITIONS "PSX_BUILD_REV=\"${PSX_GIT_REV}\""
)
# Stamp the lobby pin next to the exe (and, on multi-config, in the build
# root). Packagers must ship VERSION == this stamp — rewriting VERSION after
# the build caused Twisted Metal 4 installs where VERSION said 0.3.8 but the
# binary still filtered lobbies as 0.3.7.
#
# Single-config (Ninja/Make/MinGW): TARGET_FILE_DIR == CMAKE_BINARY_DIR, so a
# second GENERATE to the same path is rejected ("Files to be generated by
# multiple different commands"). Multi-config (VS): exe lives in Release/,
# so also drop a copy at the build root for packager lookup.
# Once per build tree, not per target: with several runtime targets
# (psx-runtime + psx-oracle + psx-beetle) each TARGET_FILE_DIR genex is a
# DIFFERENT command producing the SAME path on single-config generators,
# which CMake rejects even when the content is identical (the ae9e6a4e
# hotfix fixed the same-target duplicate but not the multi-target one).
# All runtime targets in one tree share one PSXRT_GAME_VERSION, so a
# single stamp is the correct semantics anyway.
get_property(_psxrt_ver_stamped GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED)
if(NOT _psxrt_ver_stamped)
set_property(GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED 1)
file(GENERATE
OUTPUT "$<TARGET_FILE_DIR:${target}>/psx_game_version.txt"
CONTENT "${PSXRT_GAME_VERSION}\n"
)
get_property(_psxrt_ver_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(_psxrt_ver_multi)
file(GENERATE
OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt"
CONTENT "${PSXRT_GAME_VERSION}\n"
)
endif()
endif()
# OpenBIOS is part of the native runtime product, not a developer-machine
# prerequisite. Stage both the exact ROM consumed by the compiled backend
# and its required MIT notice beside executables linking that backend.
# Desired stems may include unavailable backends; retail-only SDKs have
# no OpenBIOS backend or redistributable image. Packagers copy this
# directory as a unit when OpenBIOS is actually linked.
if(NOT PSXRT_ORACLE)
list(FIND _psxrt_bios_linked "OpenBIOS" _psxrt_openbios_index)
if(NOT _psxrt_openbios_index EQUAL -1)
if(NOT EXISTS "${PSXRECOMP_BUNDLED_BIOS_SOURCE}")
message(FATAL_ERROR
"Bundled OpenBIOS image is missing: "
"${PSXRECOMP_BUNDLED_BIOS_SOURCE}")
endif()
if(NOT EXISTS "${PSXRECOMP_BUNDLED_BIOS_LICENSE}")
message(FATAL_ERROR
"Bundled OpenBIOS license is missing: "
"${PSXRECOMP_BUNDLED_BIOS_LICENSE}")
endif()
get_filename_component(
_psxrt_bundled_bios_dir
"${PSXRECOMP_BUNDLED_BIOS_PATH}"
DIRECTORY)
get_filename_component(
_psxrt_bundled_bios_license_name
"${PSXRECOMP_BUNDLED_BIOS_LICENSE}"
NAME)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:${target}>/${_psxrt_bundled_bios_dir}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PSXRECOMP_BUNDLED_BIOS_SOURCE}"
"$<TARGET_FILE_DIR:${target}>/${PSXRECOMP_BUNDLED_BIOS_PATH}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PSXRECOMP_BUNDLED_BIOS_LICENSE}"
"$<TARGET_FILE_DIR:${target}>/${_psxrt_bundled_bios_dir}/${_psxrt_bundled_bios_license_name}"
COMMENT "Staging bundled OpenBIOS image and MIT notice"
VERBATIM)
set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS
"${PSXRECOMP_BUNDLED_BIOS_SOURCE}"
"${PSXRECOMP_BUNDLED_BIOS_LICENSE}")
endif()
# Mod catalog: the framework's builtin packages AND the title's own, both
# staged into mods/bundled by _psxrt_stage_mod_catalog (see its header).
# Skipped for cosim oracles: they have no launcher and no Mods page, and
# because a cosim exe lands in the same output directory as the runtime,
# letting it stage would have it wipe and re-stage the runtime's catalog
# with only the framework half.
if(NOT PSXRT_COSIM)
foreach(_audit_dir IN LISTS PSXRT_AUDIT_MOD_DIRS)
if(NOT IS_DIRECTORY "${_audit_dir}")
message(FATAL_ERROR
"AUDIT_MOD_DIRS for target '${target}' is not a directory: ${_audit_dir}")
endif()
endforeach()
set_property(TARGET ${target} PROPERTY PSXRT_AUDIT_MOD_DIRS "${PSXRT_AUDIT_MOD_DIRS}")
_psxrt_stage_mod_catalog("${target}" "${PSXRT_PRELOADED_MODS_DIR}"
${PSXRT_EXCLUDE_BUILTIN_MODS})
endif()
endif()
if(PSXRT_ORACLE)
target_compile_definitions(${target} PRIVATE PSX_ORACLE_BUILD=1)
else()
target_compile_definitions(${target} PRIVATE
PSX_NATIVE_BUILD=1
PSX_ENABLE_BLOCK_CYCLES=1
)
endif()
if(PSX_SHELLWIN_INTERP)
target_compile_definitions(${target} PRIVATE PSX_SHELLWIN_INTERP_DEFAULT=1)
endif()
if(PSX_GL_BATCH_IMPL STREQUAL "HLE")
target_compile_definitions(${target} PRIVATE PSX_GL_BATCH_HLE=1)
endif()
# Developer-channel mod features do not ship. A contributor reaches them by
# cloning the repo and building locally; a release build must not carry
# them at all -- not hidden behind a toggle, absent. Keying the default off
# $CI matches what the packagers already do for EXCLUDE_DEV_MODS, so one
# rule covers the catalog on disk and the catalog in the binary.
if(NOT DEFINED PSX_MOD_DEVELOPER_CHANNEL)
if(DEFINED ENV{CI} AND NOT "$ENV{CI}" STREQUAL "")
set(PSX_MOD_DEVELOPER_CHANNEL OFF)
else()
set(PSX_MOD_DEVELOPER_CHANNEL ON)
endif()
endif()
if(PSX_MOD_DEVELOPER_CHANNEL)
target_compile_definitions(${target} PRIVATE PSX_MOD_DEVELOPER_CHANNEL=1)
endif()
if(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
target_compile_definitions(${target} PRIVATE PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE=1)
endif()
if(has_game_dispatch)
target_compile_definitions(${target} PRIVATE PSX_HAS_GAME_DISPATCH=1)
endif()
if(has_game_dispatch_native_ok)
# Only the compatibility translation unit needs to know whether the
# generated dispatcher supplies the modern exact-range predicate.
# Keeping this off the target-wide definitions avoids recompiling the
# entire runtime when an existing game regenerates its dispatcher.
set_property(SOURCE
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
APPEND PROPERTY COMPILE_DEFINITIONS
PSX_GAME_DISPATCH_HAS_NATIVE_OK=1)
endif()
if(has_game_dispatch_native_ok_full)
set_property(SOURCE
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
APPEND PROPERTY COMPILE_DEFINITIONS
PSX_GAME_DISPATCH_HAS_NATIVE_OK_FULL=1)
endif()
if(has_overlay_dispatch)
target_compile_definitions(${target} PRIVATE PSX_HAS_OVERLAY_DISPATCH=1)
endif()
# PSX_DEBUG_TOOLS option declared at the top of runtime.cmake so it's
# also visible to psx-beetle / non-runtime-helper targets.
if(NOT PSX_DEBUG_TOOLS)
target_compile_definitions(${target} PRIVATE PSX_NO_DEBUG_TOOLS=1)
endif()
if(PSXRECOMP_HAS_RECOMP_NET)
target_compile_definitions(${target} PRIVATE PSX_HAS_RECOMP_NET=1)
target_link_libraries(${target} PRIVATE recomp_net)
if(PSXRECOMP_HAS_RBENGINE)
target_link_libraries(${target} PRIVATE retcomm_rbengine)
endif()
endif()
if(PSXRECOMP_HAS_RBENGINE_SNAP)
target_compile_definitions(${target} PRIVATE PSX_HAS_RBENGINE_SNAP=1)
endif()
if(PSXRECOMP_HAS_LOBBY_CLIENT)
target_compile_definitions(${target} PRIVATE PSX_HAS_LOBBY_CLIENT=1)
endif()
if(PSX_SETUP_WIZARD)
target_compile_definitions(${target} PRIVATE PSX_HAS_SETUP_WIZARD=1)
endif()
# First-divergence co-sim oracle (COSIM_ORACLE.md): the clean, deterministic build.
# PSX_COSIM activates the cosim engine/hooks; PSX_NO_DEBUG_TOOLS strips ALL the laggy
# diagnostic tooling (the debug server thread, per-block recording, rings) so the run
# is single-threaded + fast + deterministic. The two instances (this + a FORCE_INTERP
# run) are driven in cycle-lockstep by tools/cosim.py.
if(PSXRT_COSIM)
target_compile_definitions(${target} PRIVATE PSX_COSIM=1 PSX_NO_DEBUG_TOOLS=1)
endif()
# Shared recomp-ui Dear ImGui launcher (not in the oracle build — that's headless).
# Lives at the game repo root (RECOMP_UI_ROOT / CMAKE_SOURCE_DIR/recomp-ui),
# not under psxrecomp/lib/.
if(PSX_RECOMP_UI AND NOT PSXRT_ORACLE)
if(NOT RECOMP_UI_ROOT OR NOT EXISTS "${RECOMP_UI_ROOT}/recomp_ui.cmake")
message(FATAL_ERROR
"PSX_RECOMP_UI=ON but recomp-ui is missing from the game "
"repo root.\n"
"A source ZIP downloaded from GitHub never contains "
"submodule contents and cannot build. Clone instead:\n"
" git clone --recurse-submodules <repo-url>\n"
"In a clone that already declares recomp-ui, fetch the "
"pinned commit:\n"
" git submodule update --init --recursive\n"
"Only if this repo does not declare recomp-ui yet, add it "
"(use the fork this project pins, not necessarily "
"upstream):\n"
" git submodule add <recomp-ui-url> recomp-ui\n"
"Or point at an existing checkout: "
"-DRECOMP_UI_ROOT=/path/to/recomp-ui")
endif()
# recomp-ui gates its Mods view behind RECOMP_UI_ENABLE_MODS, which
# defaults OFF there -- correct for a cross-console launcher, since a
# console with no mod system should not show an empty Mods tab.
#
# psxrecomp does ship mod packages as a first-class, documented feature
# (docs/MOD_PACKAGES.md; Tomba! and Ape Escape ship catalogs today), so
# the framework opts in on every title's behalf. Without this, a title
# that already shipped a Mods panel silently loses it on its next
# rebuild -- the panel compiles in but stays inert, which reads as "this
# build is old" rather than as a missing build flag.
#
# Set before the include so recomp-ui's option() honours it, and only
# when the caller has not already decided, so -DRECOMP_UI_ENABLE_MODS=OFF
# still wins.
if(NOT DEFINED RECOMP_UI_ENABLE_MODS)
set(RECOMP_UI_ENABLE_MODS ON CACHE BOOL
"Enable the recomp-ui Mods view (psxrecomp ships mod packages)")
endif()
# The seed above only fires on a FRESH cache. A build tree configured
# before it existed already has RECOMP_UI_ENABLE_MODS=OFF in its cache,
# written by recomp-ui's own option(), and nothing can distinguish that
# stale default from a deliberate -DRECOMP_UI_ENABLE_MODS=OFF. Such a
# tree therefore keeps producing a Mods-less build across reconfigures,
# which is the same "reads as a stale build" failure the seed was added
# to prevent -- just one level up, and invisible.
#
# The explicit-OFF contract above is deliberate, so this does not
# override it. It makes the state audible instead: whoever sees a
# Mods-less build now gets told why and how to change it, rather than
# having to query the running game to discover the panel was compiled
# out. Deliberate opt-outs get one line per configure, which is the
# price of the two cases being genuinely indistinguishable.
if(NOT RECOMP_UI_ENABLE_MODS)
message(WARNING
"RECOMP_UI_ENABLE_MODS is OFF in this build tree, so the "
"launcher will have NO Mods page even for a title that ships a "
"catalog.\n"
" If that was not deliberate, this cache predates the "
"framework opting in: delete CMakeCache.txt (or just that "
"entry) to pick up the ON default.")
endif()
set(RECOMP_UI_SDL3 ${PSX_SDL3})
include("${RECOMP_UI_ROOT}/recomp_ui.cmake")
# Asset staging is console-scoped in recomp-ui. Select PSX once here so
# every game using this framework ships only PlayStation launcher art
# (plus common chrome), never unrelated NES/N64/etc. assets.
set(_psx_recomp_ui_args CONSOLE psx)
if(PSXRT_LAUNCHER_BOXART)
list(APPEND _psx_recomp_ui_args BOXART "${PSXRT_LAUNCHER_BOXART}")
endif()
if(PSXRT_LAUNCHER_PAD)
list(APPEND _psx_recomp_ui_args PAD "${PSXRT_LAUNCHER_PAD}")
endif()
if(PSXRT_LAUNCHER_BRAND)
list(APPEND _psx_recomp_ui_args BRAND "${PSXRT_LAUNCHER_BRAND}")
endif()
recomp_target_launcher_ui(${target} ${_psx_recomp_ui_args})
target_compile_definitions(${target} PRIVATE
RECOMP_UI_PSX_HAS_REWIND=$<BOOL:${PSXRECOMP_HAS_RBENGINE_SNAP}>)
endif()
if(PSX_OPENXR)
target_compile_definitions(${target} PRIVATE PSX_OPENXR=1)
target_link_libraries(${target} PRIVATE openxr_loader)
endif()
if(WIN32 OR MINGW)
# opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
# iphlpapi: GetAdaptersAddresses in the launcher's netplay
# local-address discovery (MSVC does not link it implicitly).
target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32 shell32)
# Newer mingw-w64 maps clock_gettime → clock_gettime64 in libwinpthread.
# Link it even when netplay code prefers Win32 clocks, so any residual
# POSIX time refs (third-party / debug tools) resolve under -static.
if(MINGW)
target_link_libraries(${target} PRIVATE winpthread)
endif()
else()
if(CMAKE_DL_LIBS)
target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS})
endif()
# GL for gpu_gl_renderer.c. Do NOT hardcode OpenGL::GL: FindOpenGL
# reports OpenGL as found on a GLVND host that has libOpenGL.so but no
# legacy libGL.so / glx.h (Steam Deck), yet never creates that target —
# which then fails at generate time. recomp_resolve_gl() picks whatever
# the host actually has; see recomp-ui/cmake/recomp_gl.cmake.
if(NOT COMMAND recomp_resolve_gl
AND RECOMP_UI_ROOT AND EXISTS "${RECOMP_UI_ROOT}/cmake/recomp_gl.cmake")
include("${RECOMP_UI_ROOT}/cmake/recomp_gl.cmake")
endif()
if(COMMAND recomp_resolve_gl)
recomp_resolve_gl(_psx_gl_target)
target_link_libraries(${target} PRIVATE ${_psx_gl_target})
else()
# No recomp-ui checkout (PSX_RECOMP_UI=OFF): same resolution inline.
find_package(OpenGL)
if(TARGET OpenGL::GL)
target_link_libraries(${target} PRIVATE OpenGL::GL)
elseif(TARGET OpenGL::OpenGL)
target_link_libraries(${target} PRIVATE OpenGL::OpenGL)
elseif(OPENGL_gl_LIBRARY)
target_link_libraries(${target} PRIVATE "${OPENGL_gl_LIBRARY}")
elseif(OPENGL_opengl_LIBRARY)
target_link_libraries(${target} PRIVATE "${OPENGL_opengl_LIBRARY}")
endif()
endif()
# The overlay autocompile watcher (autocompile.c), the debug server and
# the lobby client all use pthread on Unix.
find_package(Threads REQUIRED)
target_link_libraries(${target} PRIVATE Threads::Threads)
# musl declares getcontext/makecontext/swapcontext (psx_fiber.c) but
# does not implement them; libucontext does.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
include(CheckSymbolExists)
check_symbol_exists(getcontext "ucontext.h" PSX_HAVE_GETCONTEXT)
if(NOT PSX_HAVE_GETCONTEXT)
find_library(PSX_UCONTEXT_LIBRARY ucontext)
if(NOT PSX_UCONTEXT_LIBRARY)
message(FATAL_ERROR
"getcontext is missing (musl?): install libucontext")
endif()
target_link_libraries(${target} PRIVATE "${PSX_UCONTEXT_LIBRARY}")
endif()
endif()
endif()
# ---- Vulkan backend (gpu_vk_renderer.c) --------------------------------
# Vulkan is loaded ENTIRELY dynamically via SDL_Vulkan_LoadLibrary +
# vkGetInstanceProcAddr (mirroring how the GL backend loads modern GL through
# SDL), so there is NO link-time dependency on vulkan-1: the self-contained
# static exe is preserved and a machine without a Vulkan ICD still launches
# (the backend reports init failure and the runtime falls back to software).
# We need only the Vulkan HEADERS at compile time, plus glslc to build SPIR-V.
# Both ship with the Vulkan SDK ($VULKAN_SDK) or a Vulkan-Headers package.
#
# Build Vulkan when its SDK tools are available so release binaries can offer
# it without game projects opting in individually. This does not select the
# runtime renderer: OpenGL remains the default in config_loader.h. Builders
# can still use -DPSX_ENABLE_VULKAN=OFF to produce the inert stub explicitly.
option(PSX_ENABLE_VULKAN "Build the Vulkan renderer backend when SDK tools are available" ON)
if(PSX_ENABLE_VULKAN)
# $VULKAN_SDK first; else find_path. Unset before find_path — an empty
# normal _vk_inc makes find_path a no-op on modern CMake (Homebrew miss).
set(_vk_inc "")
if(DEFINED ENV{VULKAN_SDK})
if(EXISTS "$ENV{VULKAN_SDK}/Include/vulkan/vulkan.h")
set(_vk_inc "$ENV{VULKAN_SDK}/Include")
elseif(EXISTS "$ENV{VULKAN_SDK}/include/vulkan/vulkan.h")
set(_vk_inc "$ENV{VULKAN_SDK}/include")
endif()
endif()
if(NOT _vk_inc)
unset(_vk_inc CACHE)
unset(_vk_inc)
find_path(_vk_inc vulkan/vulkan.h)
endif()
find_program(GLSLC_EXE NAMES glslc
HINTS "$ENV{VULKAN_SDK}/Bin" "$ENV{VULKAN_SDK}/bin")
# Finding the header is not the same as being able to include it. The usual
# miss is _vk_inc=/usr/include: CMake drops that directory from include
# lists (adding it explicitly would disturb the system header order), so
# the -I never reaches the compiler, and a sysroot toolchain never had it
# on the search path to begin with. Verify, then fall back to the inert
# stub this block already knows how to build.
set(_vk_stage "")
if(_vk_inc AND GLSLC_EXE)
_psx_header_compiles(_psx_vk_ok "vulkan/vulkan.h" INCLUDES "${_vk_inc}")
if(NOT _psx_vk_ok)
# Unreachable as given. Rather than give up the renderer, stage a
# private include dir holding ONLY the Vulkan subtrees and try that:
# it is not /usr/include, so CMake will emit it, and it pulls none
# of the rest of the host include root ahead of the sysroot's own
# libc headers -- which is what makes this safe where adding
# ${_vk_inc} itself would not be. vk_video/ has to come along too;
# vulkan_core.h includes the H.264/H.265 codec headers from it, so
# staging vulkan/ alone still fails to compile.
string(SHA256 _vk_stage_key "${_vk_inc}")
string(SUBSTRING "${_vk_stage_key}" 0 12 _vk_stage_key)
set(_vk_stage "${CMAKE_CURRENT_BINARY_DIR}/${target}_vkinc_${_vk_stage_key}")
file(MAKE_DIRECTORY "${_vk_stage}")
foreach(_vk_sub vulkan vk_video)
if(EXISTS "${_vk_inc}/${_vk_sub}")
if(NOT EXISTS "${_vk_stage}/${_vk_sub}" AND
NOT IS_SYMLINK "${_vk_stage}/${_vk_sub}")
file(CREATE_LINK "${_vk_inc}/${_vk_sub}"
"${_vk_stage}/${_vk_sub}" SYMBOLIC COPY_ON_ERROR)
endif()
endif()
endforeach()
_psx_header_compiles(_psx_vk_ok "vulkan/vulkan.h" INCLUDES "${_vk_stage}")
if(_psx_vk_ok)
message(STATUS
"Vulkan backend: ${_vk_inc} is not reachable directly; "
"staged ${_vk_stage} instead.")
else()
message(STATUS
"Vulkan backend: headers at ${_vk_inc} are not reachable from "
"${CMAKE_C_COMPILER} - gpu_vk_renderer.c builds as a software "
"stub. Set VULKAN_SDK to a copy the compiler can see to enable it.")
set(_vk_inc "")
set(_vk_stage "")
endif()
endif()
unset(_psx_vk_ok)
endif()
if(_vk_inc AND GLSLC_EXE)
message(STATUS "Vulkan backend: headers ${_vk_inc}, glslc ${GLSLC_EXE}")
if(_vk_stage)
target_include_directories(${target} PRIVATE "${_vk_stage}")
else()
target_include_directories(${target} PRIVATE "${_vk_inc}")
endif()
target_compile_definitions(${target} PRIVATE PSX_HAVE_VULKAN=1)
# Compile every shader under runtime/shaders/ to SPIR-V (glslc) and embed
# them into one generated header (vk_shaders_spv.h) of uint32_t arrays, so
# gpu_vk_renderer.c creates shader modules with no runtime file deps.
# Resolve the native interpreter behind the Windows Python launcher.
# Invoking py.exe with a .py file can honor that file's /usr/bin/env
# shebang and accidentally select an unrelated MSYS Python, which cannot
# open the native absolute paths emitted by Windows CMake/Ninja.
if(WIN32 AND NOT PSX_PYTHON)
find_program(_psx_python_launcher NAMES py)
if(_psx_python_launcher)
execute_process(
COMMAND "${_psx_python_launcher}" -3 -c
"import sys; print(sys.executable)"
OUTPUT_VARIABLE _psx_native_python
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(EXISTS "${_psx_native_python}")
set(PSX_PYTHON "${_psx_native_python}" CACHE FILEPATH
"Python 3 interpreter used by runtime build tools")
endif()
endif()
endif()
if(NOT PSX_PYTHON)
find_program(PSX_PYTHON NAMES python3 python)
endif()
if(NOT PSX_PYTHON)
message(FATAL_ERROR
"Vulkan shader embedding requires Python 3 (py/python/python3)")
endif()
set(_vk_shader_dir "${PSXRECOMP_ROOT}/runtime/shaders")
file(GLOB _vk_shaders
"${_vk_shader_dir}/*.vert" "${_vk_shader_dir}/*.frag"
"${_vk_shader_dir}/*.comp")
set(_vk_gen_dir "${CMAKE_CURRENT_BINARY_DIR}/${target}_vkgen")
set(_vk_spv_hdr "${_vk_gen_dir}/vk_shaders_spv.h")
add_custom_command(
OUTPUT "${_vk_spv_hdr}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_vk_gen_dir}"
COMMAND "${PSX_PYTHON}"
"${PSXRECOMP_ROOT}/tools/embed_spirv.py"
--glslc "${GLSLC_EXE}"
--out "${_vk_spv_hdr}"
${_vk_shaders}
DEPENDS ${_vk_shaders}
"${PSXRECOMP_ROOT}/tools/embed_spirv.py"
COMMENT "Compiling + embedding Vulkan SPIR-V shaders"
VERBATIM)
add_custom_target(${target}_vk_shaders DEPENDS "${_vk_spv_hdr}")
add_dependencies(${target} ${target}_vk_shaders)
target_include_directories(${target} PRIVATE "${_vk_gen_dir}")
else()
message(STATUS "Vulkan backend: PSX_ENABLE_VULKAN=ON but SDK headers/glslc "
"not found - gpu_vk_renderer.c builds as a software stub")
endif()
else()
message(STATUS "Vulkan backend: disabled (PSX_ENABLE_VULKAN=OFF) - "
"gpu_vk_renderer.c builds as an inert stub")
endif()
# Prefer BSS for zero-init data. MinGW+LTO has emitted multi‑MiB rings into
# .rdata as stored zeros (~150MiB MotK .exe bloat); pair with PSX_BSS on the
# largest arrays (see runtime/include/psx_bss.h).
if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
target_compile_options(${target} PRIVATE
$<$<COMPILE_LANGUAGE:C>:-fzero-initialized-in-bss>
$<$<COMPILE_LANGUAGE:CXX>:-fzero-initialized-in-bss>)
endif()
# Netplay compares guest state bit-for-bit across hosts. Clang contracts
# a*b+c into an FMA by default on arm64 (-ffp-contract=on) while x86-64
# GCC without -mfma never fuses, so the software rasterizer's float edge /
# shading math produced +-1 colour steps on single pixels between a Mac
# and a Windows peer (VRAM, hence the av digest, diverged from ~sim 1800
# in every cross-machine R4 race; rollback baselines then aborted on
# "baseline av mismatch" and the realigns forked the session). Keep IEEE
# results identical everywhere: never contract.
if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
target_compile_options(${target} PRIVATE
$<$<COMPILE_LANGUAGE:C>:-ffp-contract=off>
$<$<COMPILE_LANGUAGE:CXX>:-ffp-contract=off>)
endif()
if(MINGW)
target_link_options(${target} PRIVATE -Wl,--stack,67108864)
# No console window in Release MinGW builds.
target_link_options(${target} PRIVATE $<$<CONFIG:Release>:-mwindows>)
if(PSX_STATIC_RUNTIME)
# Fold the GCC / C++ / winpthread runtimes into the exe so it
# imports only Windows system DLLs (no libgcc_s_seh-1.dll /
# libstdc++-6.dll dependency). Pairs with the static SDL link
# above to make the exe fully self-contained. winpthread is
# linked via target_link_libraries (see WIN32 block above).
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
elseif(MSVC)
target_compile_options(${target} PRIVATE /GS- /guard:cf-)
# Visual Studio project files cannot represent language-specific target
# options on a mixed C/C++ target. Scope the experimental MSVC atomics
# switch to the one C source that needs it instead.
set_property(SOURCE ${PSXRECOMP_ROOT}/runtime/src/audio_trace.c
APPEND PROPERTY COMPILE_OPTIONS /experimental:c11atomics)
target_link_options(${target} PRIVATE /STACK:67108864,67108864 /GUARD:NO)
# No console window in Release MSVC builds. /ENTRY keeps main() as
# the entry point (not WinMain) while switching to the Windows subsystem.
target_link_options(${target} PRIVATE
$<$<CONFIG:Release>:/SUBSYSTEM:WINDOWS>
$<$<CONFIG:Release>:/ENTRY:mainCRTStartup>)
endif()
# Packages may contain data-only VCDIFF recipes for deriving a private,
# fingerprinted runtime image from the user's verified stock disc. The
# decoder is supplied by the release builder and invoked only from this
# fixed path; packages cannot provide or execute binaries.
set(PSXRECOMP_XDELTA3_EXECUTABLE "" CACHE FILEPATH
"Trusted xdelta3 executable copied beside runtime targets")
if(PSXRECOMP_XDELTA3_EXECUTABLE)
if(NOT EXISTS "${PSXRECOMP_XDELTA3_EXECUTABLE}")
message(FATAL_ERROR
"PSXRECOMP_XDELTA3_EXECUTABLE does not exist: "
"${PSXRECOMP_XDELTA3_EXECUTABLE}")
endif()
if(WIN32)
set(_psxmod_xdelta_name "xdelta3.exe")
else()
set(_psxmod_xdelta_name "xdelta3")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PSXRECOMP_XDELTA3_EXECUTABLE}"
"$<TARGET_FILE_DIR:${target}>/${_psxmod_xdelta_name}"
COMMENT "Staging trusted xdelta3 decoder for derived-disc mods"
VERBATIM)
endif()
# PGXP variant auto-clone (docs/ENHANCEMENTS.md G1.10): with
# -DPSX_PGXP_VARIANT=ON, every primary runtime target grows an
# <exe>_pgxp sibling — the SAME arguments (same generated C, extras,
# ports) compiled with -DPSX_PGXP=1 (see the PGXP option above). Done
# HERE, at the end, by replaying ARGN recursively, so it works for
# every caller — titles that call this function directly (Ape) and
# the psxrecomp_add_game_runtime wrapper alike. Oracle/cosim targets
# and the clone itself are excluded.
if(NOT PSXRT_PGXP AND NOT PSXRT_ORACLE AND NOT PSXRT_COSIM)
option(PSX_PGXP_VARIANT
"Also build the <exe>_pgxp PGXP precision-shadowing variant" OFF)
if(PSX_PGXP_VARIANT)
psxrecomp_add_runtime_target(${target}-pgxp PGXP PGXP_CLONE ${ARGN})
endif()
endif()
endfunction()
# Compatibility for early v4 game projects that used the longer helper name.
function(psxrecomp_v4_add_runtime_target target)
psxrecomp_add_runtime_target(${target} ${ARGN})
endfunction()
# ---------------------------------------------------------------------------
# High-level helper for title repos (setup-host + optional generated game C).
#
# Typical game CMakeLists.txt:
# set(PSXRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/psxrecomp")
# include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
# psxrecomp_add_game_runtime(psx-runtime
# WINDOW_TITLE "My Game Recompiled"
# GEN_MARKER "generated/SLUS_01234_dispatch.c"
# GEN_FULL_GLOB "generated/SLUS_01234_full_*.c"
# CODEGEN_SETUP_SOURCES codegen_setup.c
# DEFAULT_GAME_CONFIG_PATH "game.toml"
# LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
# APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"
# MAX_PLAYERS 2
# MEMCARD_SLOTS 1 # optional; title only uses memory card port 1
# ENABLE_NETPLAY_IF_PRESENT
# ENABLE_SETUP_WIZARD
# PRELOADED_MODS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded"
# EXCLUDE_BUILTIN_MODS psx.presentation.bezel # optional; builtins not shipped
# PGXP # optional; the runtime is the PGXP hook flavor
# )
#
# Remaining args are forwarded to psxrecomp_add_runtime_target.
# ---------------------------------------------------------------------------
function(psxrecomp_add_game_runtime target)
# PGXP: build the title's ONE runtime as the PGXP hook flavor (see the PGXP
# option of psxrecomp_add_runtime_target): -DPSX_PGXP=1, overlay flavor 2,
# no _pgxp suffix and no clone beside it. The generated C already carries
# the PGXP_*() hook sites, so committed generated/ does not change; only
# the compile define does. Parsed here rather than left to the unparsed
# tail so it cannot be swallowed by a multi-value argument written before
# it (CODEGEN_SETUP_SOURCES would take it as a source file).
set(options ENABLE_NETPLAY_IF_PRESENT ENABLE_SETUP_WIZARD PGXP)
set(oneValueArgs
GEN_MARKER
GEN_FULL_FALLBACK
VERSION_FILE
CODEGEN_SETUP_INCLUDE_DIR
NETPLAY_LOBBY_URL
PRELOADED_MODS_DIR
)
# EXCLUDE_BUILTIN_MODS is parsed here (not left in the unparsed tail) for
# the same reason as PRELOADED_MODS_DIR: an unknown keyword written after
# CODEGEN_SETUP_SOURCES would otherwise be swallowed as more source files.
set(multiValueArgs GEN_FULL_GLOB CODEGEN_SETUP_SOURCES EXCLUDE_BUILTIN_MODS)
cmake_parse_arguments(PSXG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT PSXRECOMP_ROOT)
message(FATAL_ERROR
"psxrecomp_add_game_runtime: PSXRECOMP_ROOT is not set. "
"Set it to the psxrecomp submodule path before including runtime.cmake.")
endif()
option(PSXRECOMP_FORCE_SETUP_HOST
"Build without linking game C even if generated/ exists" OFF)
option(PSXRECOMP_REQUIRE_GAME_C
"Fail configure if generated game C is missing" OFF)
# Legacy BPE option name (CI / docs may still pass -DBPE_FORCE_SETUP_HOST=ON).
if(BPE_FORCE_SETUP_HOST)
set(PSXRECOMP_FORCE_SETUP_HOST ON CACHE BOOL
"Build without linking game C even if generated/ exists" FORCE)
endif()
if(BPE_REQUIRE_GAME_C)
set(PSXRECOMP_REQUIRE_GAME_C ON CACHE BOOL
"Fail configure if generated game C is missing" FORCE)
endif()
if(NOT PSXG_VERSION_FILE)
set(PSXG_VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/VERSION")
endif()
if(NOT IS_ABSOLUTE "${PSXG_VERSION_FILE}")
set(PSXG_VERSION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${PSXG_VERSION_FILE}")
endif()
# Lobby / release pin from VERSION when PSX_GAME_VERSION is unset.
if(EXISTS "${PSXG_VERSION_FILE}")
file(READ "${PSXG_VERSION_FILE}" _psxg_ver_raw)
string(STRIP "${_psxg_ver_raw}" _psxg_release_version)
else()
set(_psxg_release_version "0.0.0")
endif()
set(PSX_GAME_VERSION "" CACHE STRING
"Lobby release pin (empty = Release uses VERSION file, else dev)")
if(PSX_GAME_VERSION STREQUAL "")
get_property(_psxg_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(_psxg_is_multi)
set(PSX_GAME_VERSION
"$<IF:$<CONFIG:Release>,${_psxg_release_version},dev>")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(PSX_GAME_VERSION "${_psxg_release_version}")
else()
set(PSX_GAME_VERSION "dev")
endif()
endif()
message(STATUS
"psxrecomp game_version: ${PSX_GAME_VERSION} "
"(from VERSION=${_psxg_release_version})")
if(NOT "${PSX_GAME_VERSION}" STREQUAL ""
AND NOT "${PSX_GAME_VERSION}" MATCHES "\\$<"
AND NOT "${_psxg_release_version}" STREQUAL ""
AND NOT "${_psxg_release_version}" STREQUAL "0.0.0"
AND NOT "${PSX_GAME_VERSION}" STREQUAL "${_psxg_release_version}")
message(WARNING
"PSX_GAME_VERSION=${PSX_GAME_VERSION} differs from VERSION file "
"(${_psxg_release_version}). Sticky CMakeCache after a VERSION bump "
"causes netplay lobby list mismatches. Reconfigure with "
"-DPSX_GAME_VERSION=${_psxg_release_version} or delete the build cache.")
endif()
# Use a game-root recomp-ui only when the caller did not select one.
if((NOT RECOMP_UI_ROOT OR RECOMP_UI_ROOT STREQUAL "")
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui/recomp_ui.cmake")
set(RECOMP_UI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui" CACHE PATH
"Path to recomp-ui launcher" FORCE)
endif()
# Optional: enable netplay when recomp-net is present.
if(PSXG_ENABLE_NETPLAY_IF_PRESENT)
if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
if(NOT RECOMP_NET_ROOT)
set(RECOMP_NET_ROOT "${PSXRECOMP_ROOT}/lib/recomp-net" CACHE PATH
"Path to recomp-net; empty = auto-discover" FORCE)
endif()
if(NOT DEFINED PSX_NETPLAY)
set(PSX_NETPLAY ON)
endif()
endif()
endif()
# ENABLE_NETPLAY_IF_PRESENT is nearly always a no-op, and used to be one
# silently. By the time this function runs, runtime.cmake has already been
# INCLUDED, and the include resolved recomp-net and decided whether the
# netplay TUs compile for real or as stubs (see the PSX_NETPLAY block near
# the top of this file). The option(PSX_NETPLAY ... OFF) up there has also
# already made the `NOT DEFINED` test above false. So the flag can only
# ever matter to a caller that set PSX_NETPLAY before the include -- which
# is exactly what the scaffold's pre-include block does, and that block is
# the real switch. Say so instead of leaving the caller to discover from a
# stubbed binary that the flag they passed did nothing.
if(PSXG_ENABLE_NETPLAY_IF_PRESENT AND NOT PSX_NETPLAY)
if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
message(WARNING
"ENABLE_NETPLAY_IF_PRESENT was passed but PSX_NETPLAY is OFF, so "
"netplay stays stubbed. This argument is read after "
"runtime.cmake has already been included and wired netplay, so "
"it cannot turn it on by itself. Set it BEFORE the include:\n"
" if(EXISTS \"\${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt\")\n"
" set(PSX_NETPLAY ON CACHE BOOL \"\" FORCE)\n"
" endif()")
else()
message(WARNING
"ENABLE_NETPLAY_IF_PRESENT was passed but recomp-net is not "
"checked out (${PSXRECOMP_ROOT}/lib/recomp-net), so netplay "
"stays stubbed. Run: git -C psxrecomp submodule update --init "
"lib/recomp-net")
endif()
endif()
# Title default lobby WebSocket URL (compile-time; env PSX_NET_LOBBY_URL wins).
if(PSXG_NETPLAY_LOBBY_URL)
set(PSX_NET_LOBBY_DEFAULT_URL "${PSXG_NETPLAY_LOBBY_URL}" CACHE STRING
"Compile-time default lobby URL (ws://host:port)" FORCE)
endif()
# Optional: advertise first-run wizard + Generate & rebuild.
# Prefer setting -DPSX_SETUP_WIZARD=ON before include(runtime.cmake) so the
# option() default does not stick OFF in an existing cache; this helper
# still forces ON when the title lists ENABLE_SETUP_WIZARD.
if(PSXG_ENABLE_SETUP_WIZARD)
set(PSX_SETUP_WIZARD ON CACHE BOOL
"Advertise first-run setup wizard + Generate & rebuild in recomp-ui"
FORCE)
endif()
# Setup-host CI (-DPSXRECOMP_FORCE_SETUP_HOST=ON) without the wizard ships a
# zip that never opens first-run / Generate & rebuild (BPE regression).
if(PSXRECOMP_FORCE_SETUP_HOST AND NOT PSX_SETUP_WIZARD)
message(FATAL_ERROR
"PSXRECOMP_FORCE_SETUP_HOST=ON requires PSX_SETUP_WIZARD=ON.\n"
"Add ENABLE_SETUP_WIZARD to psxrecomp_add_game_runtime(...), and/or:\n"
" set(PSX_SETUP_WIZARD ON CACHE BOOL \"…\" FORCE)\n"
"before include(runtime.cmake), and/or pass -DPSX_SETUP_WIZARD=ON\n"
"on the cmake command line (setup-release CI does this).")
endif()
if(NOT PSXG_GEN_MARKER)
message(FATAL_ERROR
"psxrecomp_add_game_runtime: GEN_MARKER is required "
"(e.g. generated/SLUS_01234_dispatch.c)")
endif()
if(NOT IS_ABSOLUTE "${PSXG_GEN_MARKER}")
set(_psxg_marker "${CMAKE_CURRENT_SOURCE_DIR}/${PSXG_GEN_MARKER}")
else()
set(_psxg_marker "${PSXG_GEN_MARKER}")
endif()
set(_psxg_has_game_c FALSE)
if(EXISTS "${_psxg_marker}" AND NOT PSXRECOMP_FORCE_SETUP_HOST)
set(_psxg_has_game_c TRUE)
endif()
if(NOT _psxg_has_game_c)
set(PSXRECOMP_ALLOW_NO_BIOS ON CACHE BOOL
"Allow runtime with no BIOS backends (setup host)" FORCE)
endif()
if(PSXRECOMP_REQUIRE_GAME_C AND NOT _psxg_has_game_c)
message(FATAL_ERROR
"PSXRECOMP_REQUIRE_GAME_C=ON but ${_psxg_marker} is missing. "
"Generate with psxrecomp-game, or leave REQUIRE_GAME_C off to "
"build the setup host.")
endif()
set(_psxg_extras)
# psxrecomp_codegen_host.c unconditionally includes recomp_launcher.h, so
# the title's setup host and the shared host implementation can only be
# built alongside the recomp-ui submodule (PSX_RECOMP_UI).
if(PSX_RECOMP_UI AND PSXG_CODEGEN_SETUP_SOURCES)
list(APPEND _psxg_extras ${PSXG_CODEGEN_SETUP_SOURCES})
list(APPEND _psxg_extras
"${PSXRECOMP_ROOT}/host/psxrecomp_codegen_host.c")
endif()
set(_psxg_forwarded_args ${PSXG_UNPARSED_ARGUMENTS})
if(PSXG_PGXP)
list(APPEND _psxg_forwarded_args PGXP)
endif()
if(NOT "${PSXG_PRELOADED_MODS_DIR}" STREQUAL "")
list(APPEND _psxg_forwarded_args
PRELOADED_MODS_DIR "${PSXG_PRELOADED_MODS_DIR}")
endif()
if(PSXG_EXCLUDE_BUILTIN_MODS)
# Hand back any forwarded keyword (and its values) the multi-value
# parse swallowed after EXCLUDE_BUILTIN_MODS; see the helper.
psx_split_exclude_builtin_mods(_psxg_exclude_ids _psxg_exclude_tail
${PSXG_EXCLUDE_BUILTIN_MODS})
if(_psxg_exclude_ids)
list(APPEND _psxg_forwarded_args
EXCLUDE_BUILTIN_MODS ${_psxg_exclude_ids})
endif()
list(APPEND _psxg_forwarded_args ${_psxg_exclude_tail})
endif()
set(_psxg_rt_args
GAME_VERSION "${PSX_GAME_VERSION}"
${_psxg_forwarded_args}
EXTRAS_SOURCES ${_psxg_extras}
)
if(_psxg_has_game_c)
message(STATUS
"psxrecomp: linking generated game C (full runtime) — ${_psxg_marker}")
set(_psxg_full_list "")
foreach(_glob IN LISTS PSXG_GEN_FULL_GLOB)
if(NOT IS_ABSOLUTE "${_glob}")
set(_glob "${CMAKE_CURRENT_SOURCE_DIR}/${_glob}")
endif()
file(GLOB _hits "${_glob}")
list(APPEND _psxg_full_list ${_hits})
endforeach()
if(NOT _psxg_full_list)
if(PSXG_GEN_FULL_FALLBACK)
if(NOT IS_ABSOLUTE "${PSXG_GEN_FULL_FALLBACK}")
set(_psxg_full_list
"${CMAKE_CURRENT_SOURCE_DIR}/${PSXG_GEN_FULL_FALLBACK}")
else()
set(_psxg_full_list "${PSXG_GEN_FULL_FALLBACK}")
endif()
else()
# Derive SLUS_*_full.c next to the dispatch marker.
get_filename_component(_psxg_marker_dir "${_psxg_marker}" DIRECTORY)
get_filename_component(_psxg_marker_name "${_psxg_marker}" NAME)
string(REPLACE "_dispatch.c" "_full.c" _psxg_full_name
"${_psxg_marker_name}")
set(_psxg_full_list "${_psxg_marker_dir}/${_psxg_full_name}")
endif()
endif()
psxrecomp_add_runtime_target(${target}
GAME_GENERATED_FULL_C ${_psxg_full_list}
GAME_GENERATED_DISPATCH_C "${_psxg_marker}"
${_psxg_rt_args}
)
# psxrecomp_add_runtime_target auto-clones a ${target}-pgxp sibling
# when PSX_PGXP_VARIANT is ON; fold it into the tail configuration
# below so it gets the same game-codegen defines and includes.
set(_psxg_targets ${target})
if(TARGET ${target}-pgxp)
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()
foreach(_psxg_t IN LISTS _psxg_targets)
target_compile_definitions(${_psxg_t} PRIVATE PSX_HAS_GAME_CODEGEN=1)
# Distinct from PSX_HAS_GAME_CODEGEN (which just means "generated game
# C is linked"): this only fires when a codegen_setup.c-style host was
# actually provided, since that file needs recomp-ui/launcher headers
# that a --no-recomp-ui build does not have.
if(PSX_RECOMP_UI AND PSXG_CODEGEN_SETUP_SOURCES)
target_compile_definitions(${_psxg_t} PRIVATE PSX_HAS_CODEGEN_SETUP_HOST=1)
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 <version> can pick
# up the repo VERSION pin file. Title codegen_setup.h is found via
# quote-include next to codegen_setup.c.
set(_psxg_inc "${PSXRECOMP_ROOT}/host")
if(PSXG_CODEGEN_SETUP_INCLUDE_DIR)
list(APPEND _psxg_inc "${PSXG_CODEGEN_SETUP_INCLUDE_DIR}")
endif()
if(RECOMP_UI_ROOT)
list(APPEND _psxg_inc "${RECOMP_UI_ROOT}/src")
endif()
foreach(_psxg_t IN LISTS _psxg_targets)
target_include_directories(${_psxg_t} PRIVATE ${_psxg_inc})
endforeach()
endfunction()