Files
psxrecomp/runtime/runtime.cmake
T
Matthew Stanley eb262c4b23 feat(mods): let a trusted plugin read its own option values
The manifest schema already carries typed, validated, launcher-rendered,
persisted options ([[option]] boolean/choice/integer), and the launcher
provider already reads them. A trusted activation plugin could not: the
callback takes no arguments, so a plugin was only ever an on/off switch.

That forced a parameterised feature to be modelled as one feature per
value, and `constraint` only expresses ordered_integer WITHIN a feature, so
those pseudo-features could not even be made mutually exclusive. A game
wanting "Widescreen: 16:9 / 21:9" or "Interpolation: 90/120/144/165/240"
had no way to express it as one entry.

psx_mod_option_value(package, feature, option, out, out_size) closes the
gap against the value the player actually committed. It returns 0 with an
empty string when the plan is not committed or the ids do not resolve, so a
caller applies its own default rather than reading "" as a selection. Ids
are explicit because registration is by plugin id alone and the callback
carries no package/feature context.

Also in this commit, from the same investigation:

* Sustained overlay-autocompile failure is now LOUD. After 3 consecutive
  failed runs the runtime prints once to stdout: what failed, the exit
  code, that overlays are staying interpreted and frame times will be far
  worse than the build is capable of, what to check, and the compiler's own
  output tail. Previously the only trace was a `fails` counter reachable
  over TCP, so a wholly-interpreted title presented as "why is this slow"
  rather than as a message. Threshold is 3, not 1, because isolated
  failures (a shard racing a capture rewrite, a transient lock) are
  ordinary and self-heal. Measured cost of the silent case on Tomba!:
  p50 frame period 105ms, vs 3.1ms once shards existed.

* The autocompile child's working directory is printed at startup beside
  cache and captures. Every relative path in overlay_autocompile_cmd
  resolves against it and it comes from game.toml's detected project root,
  so a game.toml staged outside the real root silently repoints the entire
  compile command with no other symptom.

* A build tree whose cache still carries recomp-ui's OFF for
  RECOMP_UI_ENABLE_MODS now says so at configure time. 610234a's seed only
  fires on a fresh cache, and a stale OFF is indistinguishable from a
  deliberate one, so such trees kept producing Mods-less builds across
  reconfigures - the same "reads as a stale build" failure one level up,
  and invisible. The explicit-OFF contract is deliberate so this does not
  override it; it makes the state audible.

Runtime ctest 16/16. Warning verified to fire on a tree configured with
-DRECOMP_UI_ENABLE_MODS=OFF and to stay silent on a fresh configure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:33:44 -07:00

971 lines
48 KiB
CMake

# Shared psxrecomp runtime CMake helpers.
#
# Include this from either the framework runtime build or a sibling game
# project. Call psxrecomp_add_runtime_target() after SDL2 detection has
# populated SDL2_INCLUDE_DIRS and SDL2_LIBRARIES.
if(NOT DEFINED PSXRECOMP_ROOT)
get_filename_component(PSXRECOMP_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
endif()
# 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.
if(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
find_program(CCACHE_PROGRAM ccache)
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 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()
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()
# PSX_STATIC_RUNTIME: produce a 100% self-contained MinGW exe.
#
# A default MinGW build dynamically imports three NON-system DLLs —
# SDL2.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 SDL2) 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.
if(MINGW AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" ON)
else()
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" OFF)
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/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_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/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/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/bios_hle.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle_plan.c
${PSXRECOMP_ROOT}/runtime/src/savestate.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/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_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)
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)
add_subdirectory("${RECOMP_NET_ROOT}" "${CMAKE_BINARY_DIR}/recomp-net")
endif()
set(PSXRECOMP_HAS_RECOMP_NET TRUE)
message(STATUS "psxrecomp: recomp-net netplay enabled (${RECOMP_NET_ROOT})")
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)")
endif()
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()
# 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()
if(NOT _psxrt_bios_linked)
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.)")
endif()
message(STATUS "BIOS backends linked: ${_psxrt_bios_linked}")
list(LENGTH _psxrt_bios_linked _psxrt_bios_count)
# 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)
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()
function(psxrecomp_add_runtime_target target)
set(options ORACLE COSIM)
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
)
# 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)
set(generated_sources
"${PSXRT_BIOS_GENERATED_FULL_C}"
"${PSXRT_BIOS_GENERATED_DISPATCH_C}")
set_source_files_properties(${generated_sources} PROPERTIES GENERATED TRUE)
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()
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}
)
# 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()
set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}")
# ---- 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()
add_custom_target(${target}_require_generated
COMMAND ${CMAKE_COMMAND}
"-DSOURCES=${_game_generated_check}"
"-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}
${SDL2_INCLUDE_DIRS}
)
# pkg-config reports 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(SDL2_LIBRARY_DIRS)
target_link_directories(${target} PRIVATE ${SDL2_LIBRARY_DIRS})
endif()
# For a self-contained MinGW 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 SDL2_STATIC_LDFLAGS)
target_link_libraries(${target} PRIVATE ${SDL2_STATIC_LDFLAGS})
else()
target_link_libraries(${target} PRIVATE ${SDL2_LIBRARIES})
endif()
# 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()
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_BUILD_REV="${PSX_GIT_REV}"
PSX_GAME_VERSION="${PSXRT_GAME_VERSION}"
FMT_HEADER_ONLY=1
$<$<CXX_COMPILER_ID:MSVC>:SDL_MAIN_HANDLED>
)
# 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()
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_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)
endif()
if(PSXRECOMP_HAS_LOBBY_CLIENT)
target_compile_definitions(${target} PRIVATE PSX_HAS_LOBBY_CLIENT=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()
include("${RECOMP_UI_ROOT}/recomp_ui.cmake")
set(_psx_recomp_ui_args)
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})
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.
target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32)
else()
if(CMAKE_DL_LIBS)
target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS})
endif()
find_package(OpenGL)
if(OpenGL_FOUND)
target_link_libraries(${target} PRIVATE OpenGL::GL)
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)
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)
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.
find_program(PSX_PYTHON NAMES python python3)
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()
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 SDL2 link
# above to make the exe fully self-contained.
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
elseif(MSVC)
target_compile_options(${target} PRIVATE /GS- /guard:cf-)
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()
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()