Files
psxrecomp/runtime/runtime.cmake
T
Alex Vanderveen db2c03e109 hotfix for offline build deps
(cherry picked from commit 986de03fcf)
2026-08-25 11:17:44 -04:00

1934 lines
93 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()
include("${PSXRECOMP_ROOT}/cmake/psx_dependency_archive.cmake")
include("${PSXRECOMP_ROOT}/runtime/chd_dependency.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.
# RetComM 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()
find_program(CCACHE_PROGRAM NAMES ccache ccache.exe HINTS ${_psx_ccache_hints})
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()
# 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.
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 / RetComM / 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()
list(REMOVE_DUPLICATES _PSX_TOOLCHAIN_PREFIX_HINTS)
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)
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/memory.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/gpu_gl_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.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/frame_pacing.c
${PSXRECOMP_ROOT}/runtime/src/host_time.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/debug_trace_ranges.c
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.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/psx_savestate_menu.c
${PSXRECOMP_ROOT}/runtime/src/psx_rewind.c
${PSXRECOMP_ROOT}/runtime/src/host_osd.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_heartbeat.c
${PSXRECOMP_ROOT}/runtime/src/gte.cpp
${PSXRECOMP_ROOT}/runtime/src/pgxp.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/latency_ring.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/game_options.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_speed.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay.c
${PSXRECOMP_ROOT}/runtime/src/psx_lobby_client.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.)
)
# 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)")
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 lib/retcomm-rbengine\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine")
endif()
endif()
# 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"
" git submodule update --init lib/retcomm-rbengine\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)")
# 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()
# 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_registry[] = {
"
"${_psxrt_registry_entries}"
"};
"
"const uint32_t psx_bios_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: a stale-but-consistent
# BIOS still builds; opt out with -DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON.
if(NOT PSXRECOMP_SKIP_BIOS_STALE_CHECK AND _psxrt_bios_linked)
find_program(_psxrt_bash NAMES bash)
set(_psxrt_stamp "${PSXRECOMP_ROOT}/generated/${PSXRECOMP_BIOS_STEM}.emitter.sha")
if(_psxrt_bash AND EXISTS "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.sh")
execute_process(
COMMAND "${_psxrt_bash}" "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.sh"
"${PSXRECOMP_BIOS_PROFILE}"
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)
message(WARNING
"BIOS generated/ is STALE vs the recompiler emitter "
"(fingerprint mismatch).\n"
" Linking generated/${PSXRECOMP_BIOS_STEM}_*.c that may not "
"match the current emitter source, seeds, ROM or profile.\n"
" Fix: tools/regen_bios.sh --config <profile> (rebuilds "
"psxrecomp-bios + regenerates the BIOS)\n"
" (Suppress: -DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON)")
endif()
endif()
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()
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; ENHANCEMENTS.md G1.10),
# and stamp the pgxp overlay flavor so the shard cache and the ABI gate
# keep pgxp and base DLLs fully separate. The base target is untouched —
# the macros preprocess away without the define.
set(options ORACLE COSIM PGXP)
set(oneValueArgs
GAME_GENERATED_DISPATCH_C
GAME_OVERLAY_STATIC_C
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
APP_ICON
)
# 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.
set(multiValueArgs EXTRAS_SOURCES GAME_GENERATED_FULL_C)
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.
if(NOT PSXRT_DEFAULT_BIOS_PATH)
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
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 'bios/SCPH1001.BIN' instead — drop the "
"DEFAULT_BIOS_PATH argument from this game's CMakeLists.")
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
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. Inert unless a game
# provides a generated overlays_static.c — no target sets this yet.
if(PSXRT_GAME_OVERLAY_STATIC_C AND EXISTS "${PSXRT_GAME_OVERLAY_STATIC_C}")
set_source_files_properties("${PSXRT_GAME_OVERLAY_STATIC_C}" PROPERTIES GENERATED TRUE)
list(APPEND generated_sources "${PSXRT_GAME_OVERLAY_STATIC_C}")
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)
# 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.
target_compile_features(${target} PRIVATE c_std_11)
# 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)
# Distinct binary beside the base one; the launcher (or the player)
# picks the variant. Same debug port as the base build — run one at a
# time (the A/B protocol is one-toggle-per-run anyway).
set(_psxrt_exe_name "${_psxrt_exe_name}_pgxp")
target_compile_definitions(${target} PRIVATE
PSX_PGXP=1
PSX_OVERLAY_FLAVOR=2) # PSX_OVERLAY_FLAVOR_PGXP (overlay_api.h)
endif()
set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}")
# ---- Windows / desktop app icon ---------------------------------------
# Prefer an explicit APP_ICON, then the game-repo copy under assets/, then
# the framework default shipped in psxrecomp/assets (RetComM-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")
target_sources(${target} PRIVATE "${_psxrt_rc}")
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".
execute_process(
COMMAND git -C "${CMAKE_CURRENT_FUNCTION_LIST_DIR}" describe --always --dirty --tags
OUTPUT_VARIABLE PSX_GIT_REV OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(NOT PSX_GIT_REV)
set(PSX_GIT_REV "unknown")
endif()
# 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}")
target_compile_definitions(${target} PRIVATE
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}"
# 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}
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}\""
)
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 every native executable. Release
# packagers copy this directory as a unit.
if(NOT PSXRT_ORACLE)
list(FIND PSXRECOMP_BIOS_STEMS "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()
# Framework-owned mod catalog (loading speed). These target game_id "*" and
# are emulator features rather than per-disc content, so every game gets
# them without carrying a copy of the manifests. Staged BEFORE the game's
# own POST_BUILD copy so a title may still override an id if it ever needs
# to; copy_directory merges rather than replacing the tree.
if(EXISTS "${PSXRECOMP_ROOT}/mods/builtin/packages")
# Clear first: copy_directory MERGES, so a mod 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/packages, not mods/: state.toml is launcher-owned user
# state and must survive a rebuild.
set(_psx_builtin_mod_copy_commands "")
if(DEFINED PSX_BUILTIN_MOD_ALLOWLIST AND NOT
"${PSX_BUILTIN_MOD_ALLOWLIST}" STREQUAL "")
foreach(_psx_builtin_mod IN LISTS PSX_BUILTIN_MOD_ALLOWLIST)
if(NOT EXISTS "${PSXRECOMP_ROOT}/mods/builtin/packages/${_psx_builtin_mod}")
message(FATAL_ERROR
"PSX_BUILTIN_MOD_ALLOWLIST names missing package: ${_psx_builtin_mod}")
endif()
list(APPEND _psx_builtin_mod_copy_commands
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PSXRECOMP_ROOT}/mods/builtin/packages/${_psx_builtin_mod}"
"$<TARGET_FILE_DIR:${target}>/mods/packages/${_psx_builtin_mod}")
endforeach()
else()
list(APPEND _psx_builtin_mod_copy_commands
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PSXRECOMP_ROOT}/mods/builtin"
"$<TARGET_FILE_DIR:${target}>/mods")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf
"$<TARGET_FILE_DIR:${target}>/mods/packages"
${_psx_builtin_mod_copy_commands}
COMMENT "Staging framework-owned mod catalog"
VERBATIM)
unset(_psx_builtin_mod_copy_commands)
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(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.\n"
"Add at the game repo root:\n"
" git submodule add -b master "
"https://github.com/mstan/recomp-ui.git recomp-ui\n"
"Or set -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(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)
# 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()
# Async lobby connect (psx_lobby_client.c) uses pthread on Unix.
if(PSXRECOMP_HAS_LOBBY_CLIENT)
find_package(Threads REQUIRED)
target_link_libraries(${target} PRIVATE Threads::Threads)
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")
if(_vk_inc AND GLSLC_EXE)
message(STATUS "Vulkan backend: headers ${_vk_inc}, glslc ${GLSLC_EXE}")
target_include_directories(${target} PRIVATE "${_vk_inc}")
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 python python3)
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()
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 (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 ${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
# ENABLE_NETPLAY_IF_PRESENT
# ENABLE_SETUP_WIZARD
# )
#
# Remaining args are forwarded to psxrecomp_add_runtime_target.
# ---------------------------------------------------------------------------
function(psxrecomp_add_game_runtime target)
set(options ENABLE_NETPLAY_IF_PRESENT ENABLE_SETUP_WIZARD)
set(oneValueArgs
GEN_MARKER
GEN_FULL_FALLBACK
VERSION_FILE
CODEGEN_SETUP_INCLUDE_DIR
NETPLAY_LOBBY_URL
)
set(multiValueArgs GEN_FULL_GLOB CODEGEN_SETUP_SOURCES)
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()
# Prefer game-root recomp-ui (runtime.cmake also auto-discovers this).
if(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()
# 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)
list(APPEND _psxg_extras ${PSXG_CODEGEN_SETUP_SOURCES})
list(APPEND _psxg_extras
"${PSXRECOMP_ROOT}/host/psxrecomp_codegen_host.c")
endif()
set(_psxg_rt_args
GAME_VERSION "${PSX_GAME_VERSION}"
EXTRAS_SOURCES ${_psxg_extras}
${PSXG_UNPARSED_ARGUMENTS}
)
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()