# fable_2 - ReXGlue Recompiled Project
#
# SDK boilerplate lives in generated/rexglue.cmake.

cmake_minimum_required(VERSION 3.25)
project(fable_2 LANGUAGES CXX)

find_package(Python3 COMPONENTS Interpreter REQUIRED)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS constants/game_versions.json)
execute_process(COMMAND ${Python3_EXECUTABLE}
    ${CMAKE_CURRENT_SOURCE_DIR}/tools/generate_game_versions.py --check
    COMMAND_ERROR_IS_FATAL ANY)
include(constants/game_versions.cmake)
set(FABLE2_BUILD_PROFILE "${FABLE2_CATALOG_COMPATIBLE_PROFILE}" CACHE STRING "Verified guest XEX profile")
set_property(CACHE FABLE2_BUILD_PROFILE PROPERTY STRINGS
    "${FABLE2_CATALOG_COMPATIBLE_PROFILE}" ${FABLE2_CATALOG_PROFILES})
if(FABLE2_BUILD_PROFILE STREQUAL FABLE2_CATALOG_COMPATIBLE_PROFILE)
    set(FABLE2_SELECTED_PROFILES ${FABLE2_CATALOG_PROFILES})
elseif(FABLE2_BUILD_PROFILE IN_LIST FABLE2_CATALOG_PROFILES)
    set(FABLE2_SELECTED_PROFILES "${FABLE2_BUILD_PROFILE}")
else()
    message(FATAL_ERROR "Unverified FABLE2_BUILD_PROFILE: ${FABLE2_BUILD_PROFILE}")
endif()

set(CMAKE_CXX_STANDARD 23)
list(JOIN FABLE2_SELECTED_PROFILES "\", \"" FABLE2_PROFILES_JSON)
set(FABLE2_SUPPORTED_PROFILES "[\"${FABLE2_PROFILES_JSON}\"]")
configure_file(config/fable2_build.json.in ${CMAKE_CURRENT_BINARY_DIR}/fable2_build.json @ONLY)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Needed by the recomp-level patch step below (tools/apply_recomp_patches.py).
find_package(Python3 COMPONENTS Interpreter REQUIRED)

include(generated/rexglue.cmake)
set(FABLE2_CODEGEN_TOOL "" CACHE FILEPATH "Standalone compatible ReXGlue codegen tool")
if(FABLE2_CODEGEN_TOOL)
    if(NOT EXISTS "${FABLE2_CODEGEN_TOOL}")
        message(FATAL_ERROR "FABLE2_CODEGEN_TOOL does not exist")
    endif()
    # The official tool must keep its own runtime. Host headers, integration
    # sources and libraries come from the matched source-built SDK instead.
    set_target_properties(rex::rexglue PROPERTIES
        IMPORTED_LOCATION "${FABLE2_CODEGEN_TOOL}"
        IMPORTED_LOCATION_DEBUG "${FABLE2_CODEGEN_TOOL}"
        IMPORTED_LOCATION_RELWITHDEBINFO "${FABLE2_CODEGEN_TOOL}"
        IMPORTED_LOCATION_RELEASE "${FABLE2_CODEGEN_TOOL}")
    # A staged package still contains the official SDK's other configurations.
    # Reject selecting those by accident via an old/manual CMake cache.
    get_filename_component(FABLE2_PACKAGE_ROOT "${rexglue_DIR}/../../.." ABSOLUTE)
    set(FABLE2_SDK_PROVENANCE "${FABLE2_PACKAGE_ROOT}/renderer-sdk-provenance.json")
    if(WIN32 AND EXISTS "${FABLE2_SDK_PROVENANCE}")
        file(READ "${FABLE2_SDK_PROVENANCE}" FABLE2_SDK_METADATA)
        string(JSON FABLE2_STAGED_CONFIGURATION ERROR_VARIABLE FABLE2_METADATA_ERROR
               GET "${FABLE2_SDK_METADATA}" configuration)
        if(NOT FABLE2_METADATA_ERROR)
            if(CMAKE_BUILD_TYPE STREQUAL "Debug")
                set(FABLE2_REQUIRED_CONFIGURATION "Debug")
            else()
                set(FABLE2_REQUIRED_CONFIGURATION "Release")
            endif()
            if(NOT FABLE2_STAGED_CONFIGURATION STREQUAL FABLE2_REQUIRED_CONFIGURATION)
                message(FATAL_ERROR "Staged SDK is ${FABLE2_STAGED_CONFIGURATION}, but this host needs ${FABLE2_REQUIRED_CONFIGURATION}. Rerun build.cmd to select the matched SDK.")
            endif()
        endif()
    endif()
endif()

# Sources
# Strong-symbol override copies of recompiled guest functions (perf-optimized;
# regenerated by tools/make_hotfunc_overrides.py - see file headers).
# Grouped into per-category subfolders (frame/, os_notifications/, refcount/, ...);
# glob recursively so new categories are picked up automatically.
file(GLOB_RECURSE HOTFUNC_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/core/hotfunc/*.cpp)

set(FABLE2_SOURCES
    src/main.cpp
    src/core/fable2_config.cpp
    src/core/fable2_patches.cpp
    src/core/fable2_hooks.cpp
    src/core/fable2_address_scan.cpp
    ${HOTFUNC_SOURCES}
)

if(WIN32)
    add_executable(fable_2 WIN32 ${FABLE2_SOURCES})
else()
    add_executable(fable_2 ${FABLE2_SOURCES})
endif()

# The app's headers live in subfolders (src/core, src/diagnostics, src/input)
# and are included by bare filename ("fable2_config.h"), so the subfolders
# must be on the include path of both the app and the recompiled code.
set(FABLE2_HEADER_DIRS
    ${CMAKE_CURRENT_SOURCE_DIR}/src/core
    ${CMAKE_CURRENT_SOURCE_DIR}/src/diagnostics
    ${CMAKE_CURRENT_SOURCE_DIR}/src/input)
target_include_directories(fable_2 PRIVATE ${FABLE2_HEADER_DIRS})
if(TARGET fable_2_recomp)
    target_include_directories(fable_2_recomp PRIVATE ${FABLE2_HEADER_DIRS})
endif()

# Stage the Xenos GPU emulation plugin next to the exe (rexglue picks the
# per-configuration variant: rexgpu-xenosd.dll for Debug, rexgpu-xenos.dll
# for Release). Without it the game runs with no GPU emulation.
rexglue_setup_target(fable_2 GPU_PLUGINS xenos)
add_custom_command(TARGET fable_2 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "${CMAKE_CURRENT_BINARY_DIR}/fable2_build.json"
        "$<TARGET_FILE_DIR:fable_2>/fable2_build.json"
    COMMENT "Staging supported game profiles for the launcher"
    VERBATIM)

# The app's probe/diagnostic headers (src/diagnostics/*.h) use the standard CRT
# functions (fopen, getenv, strcat, strncat, ...), which the MSVC/UCRT headers
# mark deprecated when compiled with clang (-Wdeprecated-declarations noise).
# These are safe in this codebase; silence the deprecation on Windows.
target_compile_definitions(fable_2 PRIVATE
    FABLE2_BUILD_PROFILE="${FABLE2_BUILD_PROFILE}"
    FABLE2_NATIVE_CONFIGURATION="$<CONFIG>"
    FABLE2_NATIVE_SDK="$<IF:$<BOOL:${FABLE2_CODEGEN_TOOL}>,source-built,external>"
    $<$<PLATFORM_ID:Windows>:_CRT_SECURE_NO_WARNINGS>)

# Remote control server (AI input channel) is a debug-only tool: it opens a
# localhost TCP port and injects guest input, so it must NOT ship in Release /
# RelWithDebInfo builds (players shouldn't see a hidden input channel, and it's
# only useful for debugging). Defining FABLE2_REMOTE_CONTROL compiles the pad
# driver registration + server start/stop into the app; without it (non-Debug)
# those are #ifdef'd out entirely and no port is ever opened.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_definitions(fable_2 PRIVATE FABLE2_REMOTE_CONTROL)
endif()

# --- Recomp-level patches (Xenia code-region patches, made effective) ------
# This is a RECOMP: guest .text is never executed, so Xenia code patches
# (byte writes to .text) are inert at runtime. tools/apply_recomp_patches.py
# mirrors those patches into codegen's emitted C++ instead, and runs between
# codegen and compiling fable_2_recomp (keyed on the codegen stamp), so the
# patches survive codegen re-runs. Missing anchors fail the build. Skip all
# with FABLE2_RECOMP_PATCHES=0 in the build environment (A/B runs).
if(TARGET fable_2_recomp)
    add_custom_command(
        OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp"
        COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_CURRENT_SOURCE_DIR}/tools/apply_recomp_patches.py
                ${CMAKE_CURRENT_SOURCE_DIR}/generated/default
        COMMAND ${CMAKE_COMMAND} -E touch
                "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp"
        DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/codegen.build.stamp"
                ${CMAKE_CURRENT_SOURCE_DIR}/tools/apply_recomp_patches.py
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT "Applying recomp-level patches to generated code"
        VERBATIM
    )
    add_custom_target(fable_2_recomp_patches
        DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp")
    add_dependencies(fable_2_recomp fable_2_recomp_patches)
endif()

# --- Guest function-call tracing (fable2_func_trace.log) -------------------
# Rexglue codegen emits REX_FUNC_PROLOGUE() at the top of every recompiled
# guest function. Force-include src/core/fable2_func_trace.h into fable_2_recomp:
# CMake orders a target's -include after its PCH include, so the header sees
# the generated pch already loaded and redefines REX_FUNC_PROLOGUE() to also
# call Fable2FuncTraceCall(__func__) - every guest function entry then logs
# its name (see the header). No generated files are modified, so codegen
# re-runs never lose the hook. Off by default; enable with
# FABLE2_FUNC_TRACE=1 (env) or Fable2FuncTraceSetEnabled(true) (runtime).
# Appended AFTER fable_2_pch.h in the target's precompile-header list: CMake
# writes the list into cmake_pch.hxx in order, so the hook section at the
# bottom of fable2_func_trace.h (which redefines REX_FUNC_PROLOGUE) runs with
# the generated pch already loaded, and the patched macro is baked into the
# PCH used by every recompiled TU.
if(TARGET fable_2_recomp)
    target_precompile_headers(fable_2_recomp PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src/core/fable2_func_trace.h)
endif()

# NOTE: the build does NOT copy the game content (default.xex, data/, ...) into
# the build directory. fable_2.exe looks for its content in its own directory
# only; to run in place, stage it manually (one-time ~6.5 GB copy):
#   tools\stage_content.cmd <project root> out\build\<preset>
# or point the exe at an existing content tree with --game_data_root=<path>.
# saves/, cache/, logs/, and fable_2.toml are always written next to the exe.

# Stage a copy of the README + the function-tracing launcher next to the exe
# so anyone who gets the build folder (without the repo) knows how to run
# the game and how to capture guest function-call logs.
add_custom_command(TARGET fable_2 POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          ${CMAKE_CURRENT_SOURCE_DIR}/README.md
          $<TARGET_FILE_DIR:fable_2>/README.md
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          ${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2-functrace.cmd
          $<TARGET_FILE_DIR:fable_2>/fable2-functrace.cmd
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          ${CMAKE_CURRENT_SOURCE_DIR}/tools/run_profiler.cmd
          $<TARGET_FILE_DIR:fable_2>/run_profiler.cmd
  COMMENT "Staging README.md + fable2-functrace.cmd + run_profiler.cmd next to fable_2.exe"
)

# --- F5 external-Lua runner (src/lua/) -----------------------------------
# src/core/fable2_f5_lua.h runs an external script (cvar `f5_lua_path`, default
# "scripts/recomp/F5.lua") via CScriptManager::RunScript when F5 is pressed.
# The game's VFS indexes data/ from dir.manifest at startup, so the .lua file
# must exist AND be a dir.manifest line before the game starts. Sources live in
# src/lua/ (git-tracked); they are staged into BOTH the repo data/ (the
# git-tracked content root) and the exe dir (the default game data root), and
# dir.manifest is kept in sync in BOTH trees.
#
# This is a custom target that fable_2 depends on (NOT a POST_BUILD step) so it
# runs on every `build fable_2` even when the exe is up to date -- the .lua
# files are not CMake sources, so a no-op fable_2 relink would otherwise skip
# re-copying an edited script. dir.manifest syncing is idempotent: it appends
# only missing "scripts\\recomp\\<name>" entries (CRLF), leaving other entries
# untouched.
file(GLOB FABLE2_LUA_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/lua/*.lua)
add_custom_target(fable_2_lua_staging
  COMMAND ${CMAKE_COMMAND} -E copy_directory
          ${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          ${CMAKE_CURRENT_SOURCE_DIR}/data/scripts/recomp
  COMMAND ${CMAKE_COMMAND} -E copy_directory
          ${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          $<TARGET_FILE_DIR:fable_2>/data/scripts/recomp
  COMMAND ${CMAKE_COMMAND}
          -Dlua_dir=${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          -Dmanifests=${CMAKE_CURRENT_SOURCE_DIR}/data/dir.manifest
          -P ${CMAKE_CURRENT_SOURCE_DIR}/tools/ensure_recomp_manifest.cmake
  COMMAND ${CMAKE_COMMAND}
          -Dlua_dir=${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          -Dmanifests=$<TARGET_FILE_DIR:fable_2>/data/dir.manifest
          -P ${CMAKE_CURRENT_SOURCE_DIR}/tools/ensure_recomp_manifest.cmake
  DEPENDS ${FABLE2_LUA_FILES}
  COMMENT "Staging src/lua/* -> data/scripts/recomp/ + dir.manifest (content root + exe dir)"
)
add_dependencies(fable_2 fable_2_lua_staging)

# The remote-control CLI client is a debug-only feature (see the
# FABLE2_REMOTE_CONTROL gate above), so only stage it for Debug builds.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_custom_command(TARGET fable_2 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
            ${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2_control.py
            $<TARGET_FILE_DIR:fable_2>/fable2_control.py
    COMMENT "Staging fable2_control.py (debug-only remote control client) next to fable_2.exe"
  )
endif()

# (The screenshot command reads the game's rendered frame directly from the
# renderer's GPU guest output via Presenter::CaptureGuestOutput - see
# src/diagnostics/fable2_frame_capture.h - so no external capture helper is
# needed.)

# --- Recomp user config (fable2_config.toml) -------------------------------
# Header-only toml++ parser, vendored from the same version the ReXGlue SDK
# uses (thirdparty/rexglue-sdk-src/thirdparty/tomlplusplus), so the app's
# config loader does not depend on the SDK source tree being present.
target_include_directories(fable_2 SYSTEM PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/tomlplusplus/include)

# Stage the human-readable template next to the exe ONCE (configure time,
# copy-if-not-exists): it is user data, so user edits must survive rebuilds.
# The code also recreates it if it is ever deleted (keep that embedded copy
# in sync - see src/core/fable2_config.cpp).
if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/fable2_config.toml")
    file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/fable2_config.toml
         DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
endif()

# --- PDB for the Release build (crash debugging) --------------------------
# The recompiled guest functions are named sub_<guest_address>, so a PDB lets
# the debugger resolve a crash PC directly to the offending guest function
# (see tools/crash_capture.cmd + cdb). -g emits CodeView info without
# changing -O3 codegen; /DEBUG makes lld-link emit the .pdb.
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
    target_compile_options(fable_2 PRIVATE -g)
    target_link_options(fable_2 PRIVATE -Xlinker /DEBUG)
    if(TARGET fable_2_recomp)
        target_compile_options(fable_2_recomp PRIVATE -g)
    endif()
endif()

# --- Profiler-friendly build (option FABLE2_PROFILER_FRIENDLY) ------------
# Goal: let a profiler (VTune / WinDbg) attribute time to the per-frame region
# helpers (e.g. process_frame_work, compute_loop_timing in
# ProcessGameFrame_82276C30) WITHOUT inflating the whole profile.
#
# Do NOT use a global -fno-inline here: it also de-inlines the ~384k tiny
# `inline` CRRegister::compare() calls that pervade the recompiled code, turning
# them into function calls and massively overstating every guest function's cost
# (e.g. the profiling build showed ~2x the call count of the real build). Instead,
# the region helpers are marked __attribute__((noinline)) in source, so they stay
# visible while everything else inlines exactly as in the real -O3 build.
#
# This option only adds -fno-omit-frame-pointer (keeps the rbp frame chain so
# profilers can build accurate call stacks). Used by build.cmd -release
# fable_2_profiler (win-amd64-release-profiling preset).
option(FABLE2_PROFILER_FRIENDLY
    "Keep frame pointers for clean stack unwinding in profilers (region helpers are noinline in source; the 384k CR-compare calls stay inlined)" OFF)
if(FABLE2_PROFILER_FRIENDLY)
    # Keep frame pointers ONLY on the fable_2 target (which holds the noinline
    # per-frame region helpers that the profiler attributes time to). The
    # recompiled guest target (fable_2_recomp) must stay on plain -O3: applying
    # -fno-omit-frame-pointer to it changes host codegen of the mechanical guest
    # functions and corrupts guest state at startup (a stack-local object's
    # vtable/data pointer is read back as null -> unhandled guest 0x0 read AV).
    # This keeps the guest codegen identical to the real -O3 release build.
    # DIAGNOSTIC: flag fully disabled to isolate it from NO_PE_CLEAN / -O3 -DNDEBUG.
    # (temporarily no-op the frame-pointer addition)
endif()

# --- Stable-hash hardening: strip the absolute checkout path from __FILE__ --
# Panic/assert messages embed __FILE__. Map the source tree out so the path is
# relative to the repo root, making the binary hash independent of where the
# tree is checked out (part of the cert-free reproducible-build strategy).
if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(fable_2 PRIVATE
        -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
        -fdebug-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
        -fdebug-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=.)
    if(TARGET fable_2_recomp)
        target_compile_options(fable_2_recomp PRIVATE
            -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
            -fdebug-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
            -fdebug-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=.)
    endif()
endif()

# --- VS_VERSION_INFO (professional identity, stable across builds) --------
# Add a well-formed version resource to the host exe. Skipped (not fatal) when
# no resource compiler is configured (e.g. a bare MSVC-only preset).
if(WIN32 AND CMAKE_RC_COMPILER AND TARGET fable_2)
    target_sources(fable_2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools/repro/fable2_version.rc)
    file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/app_icon.rc"
         "1 ICON \"${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.ico\"\n")
    target_sources(fable_2 PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/app_icon.rc")
endif()
add_custom_command(TARGET fable_2 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png"
            "$<TARGET_FILE_DIR:fable_2>/app-icon.png"
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${CMAKE_CURRENT_SOURCE_DIR}/tools/StartWithoutAudio.cmd"
            "$<TARGET_FILE_DIR:fable_2>/StartWithoutAudio.cmd")

# --- Vulkan renderer: Stage 2 (source-built plugin, swappable at launch) --
# The prebuilt plugin above (GPU_PLUGINS xenos) is D3D12-only. The Vulkan
# backend is built from the (unmodified) SDK source via
# tools/build_sdk_vulkan.cmd (a Release build) into a separate tree. Stage it
# next to the exe as rexgpu-xenos-vulkan.dll so the game can swap to Vulkan at
# launch with --gpu_plugin=xenos-vulkan (see Fable2App::OnPreSetup) - no
# rebuild of the game required to switch backends. Release-only: the source
# plugin is a Release build (imports rexruntime.dll), which matches only the
# Release game (the Debug game links rexruntimed.dll). Skipped (with a note) if
# the source plugin hasn't been built yet.
# SDK locations, resolved relative to this repo (no hardcoded paths).
# Override on the command line with:
#   -DREXGLUE_SDK_ROOT=<prebuilt SDK root>    (win-amd64 layout: bin/, include/, lib/)
#   -DREXGLUE_SDK_SOURCE=<SDK source tree>    (optional; Vulkan plugin only)
# The defaults are created by tools/setup_sdk.cmd (prebuilt, auto-run by
# build.cmd) and tools/setup_sdk_src.cmd (source, only for the Vulkan path).
if(NOT DEFINED REXGLUE_SDK_ROOT)
    set(REXGLUE_SDK_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rexglue-sdk/win-amd64")
endif()
if(NOT DEFINED REXGLUE_SDK_SOURCE)
    set(REXGLUE_SDK_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rexglue-sdk")
endif()
set(FABLE2_SDK_SRC "${REXGLUE_SDK_SOURCE}")
set(FABLE2_SDK_ROOT "${REXGLUE_SDK_ROOT}")
# Never replace a matched SDK with DLLs left over from an older Vulkan build.
# (The candidate-existence checks below guard every copy, so the staging is
# safe regardless of whether the official standalone codegen tool is used.)
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
    # The SDK stages its outputs under its own out/win-amd64/Release/ tree
    # (REXGLUE_ROOT based). We need both the source plugin AND the source
    # (both-backends) runtime: the prebuilt 0.10.0 runtime is D3D12-only and is
    # missing the 16 Vulkan symbols the source plugin imports, so the Vulkan
    # path must run against the source runtime.
    file(GLOB FABLE2_VULKAN_PLUGIN_CANDIDATES "${FABLE2_SDK_SRC}/out/win-amd64/Release/rexgpu-xenos.dll")
    file(GLOB FABLE2_VULKAN_RUNTIME_CANDIDATES "${FABLE2_SDK_SRC}/out/win-amd64/Release/rexruntime.dll")
    # Vulkan loader (vulkan-1.dll), resolved at configure time: prefer a
    # locally staged copy (out/tooling/vulkan-loader, e.g. the official loader
    # zip), else the driver-installed System32 copy. Staged next to the exe so
    # the release folder's plugin LoadLibrary("vulkan-1.dll") resolves in place
    # even on a machine whose driver loader lives only in System32.
    file(GLOB FABLE2_VULKAN_LOADER_CANDIDATES
         "${CMAKE_CURRENT_SOURCE_DIR}/out/tooling/vulkan-loader/vulkan-1.dll"
         "$ENV{SystemRoot}/System32/vulkan-1.dll")
    if(FABLE2_VULKAN_LOADER_CANDIDATES)
        list(GET FABLE2_VULKAN_LOADER_CANDIDATES 0 FABLE2_VULKAN_LOADER)
    endif()
    if(FABLE2_VULKAN_PLUGIN_CANDIDATES AND FABLE2_VULKAN_RUNTIME_CANDIDATES)
        list(GET FABLE2_VULKAN_PLUGIN_CANDIDATES 0 FABLE2_VULKAN_PLUGIN)
        list(GET FABLE2_VULKAN_RUNTIME_CANDIDATES 0 FABLE2_VULKAN_RUNTIME)
        # Stage, next to the exe:
        #   rexgpu-xenos-vulkan.dll  - source dual-backend plugin (D3D12+Vulkan;
        #                              fable2.cmd copies it over rexgpu-xenos.dll)
        #   rexruntime-vulkan.dll    - source (both-backends) runtime
        #   rexruntime-d3d12.dll     - prebuilt 0.10.0 runtime (D3D12 backup)
        #   rexgpu-xenos-prebuilt.dll- prebuilt 0.10.0 D3D12 plugin (pristine
        #                              backup for `fable2.cmd prebuilt`)
        #   fable2.cmd               - launcher that picks the runtime + backend
        #   vulkan-1.dll             - Vulkan loader (in-place LoadLibrary)
        # The default rexruntime.dll (staged above by GPU_PLUGINS xenos) is the
        # prebuilt one, so a plain `fable_2.exe` launch stays D3D12 (0.10.0)
        # until fable2.cmd restages a source pair.
        add_custom_command(TARGET fable_2 POST_BUILD
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_PLUGIN}"  "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos-vulkan.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_RUNTIME}" "$<TARGET_FILE_DIR:fable_2>/rexruntime-vulkan.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_SDK_ROOT}/bin/rexruntime.dll" "$<TARGET_FILE_DIR:fable_2>/rexruntime-d3d12.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_SDK_ROOT}/bin/rexgpu-xenos.dll" "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos-prebuilt.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2.cmd" "$<TARGET_FILE_DIR:fable_2>/fable2.cmd"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2-uncapped.cmd" "$<TARGET_FILE_DIR:fable_2>/fable2-uncapped.cmd"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_LOADER}" "$<TARGET_FILE_DIR:fable_2>/vulkan-1.dll"
          COMMENT "Staging Vulkan plugin/runtime + D3D12 backups + loader + launcher")
    else()
        message(STATUS "Vulkan SDK not fully built (need rexgpu-xenos.dll + rexruntime.dll under ${FABLE2_SDK_SRC}/out/win-amd64/Release) - run tools/build_sdk_vulkan.cmd to enable 'fable2.cmd vulkan'")
    endif()
    if(NOT FABLE2_VULKAN_LOADER_CANDIDATES)
        message(STATUS "vulkan-1.dll not found (checked out/tooling/vulkan-loader + System32) - 'fable2.cmd vulkan' will rely on the driver-installed loader")
    endif()

    # Prefer the source-built runtime/plugin pair over the prebuilt 0.10.0
    # pair (Release only): the source tree carries local fixes the prebuilt
    # SDK lacks (e.g. the vsync present gate in the UI presenter, see
    # docs/FPS_CAP_INVESTIGATION.md). The source plugin is dual-backend, so a
    # plain `fable_2.exe` launch stays D3D12 ("any" picks D3D12).
    if(FABLE2_VULKAN_PLUGIN AND FABLE2_VULKAN_RUNTIME)
        add_custom_command(TARGET fable_2 POST_BUILD
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_RUNTIME}" "$<TARGET_FILE_DIR:fable_2>/rexruntime.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_PLUGIN}" "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos.dll"
          COMMENT "Staging source-built runtime/plugin (overrides prebuilt 0.10.0)")
    endif()

    # Final override (Release only): stage the fresh source-built pair that
    # tools/build_runtime_sdk.cmd just staged (build.cmd refreshes this on
    # every run). Placed AFTER the Vulkan source-pair block above so the most
    # recent build always wins - the Vulkan block reads the SDK's
    # out/win-amd64/Release/ tree, which goes stale whenever a D3D12-only SDK
    # rebuild happens (the two share the SDK output tree, last build wins).
    set(FABLE2_TOOLING_RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/out/tooling/runtime-sdk/win-amd64/bin")
    if(EXISTS "${FABLE2_TOOLING_RUNTIME_DIR}/rexruntime.dll" AND EXISTS "${FABLE2_TOOLING_RUNTIME_DIR}/rexgpu-xenos.dll")
        add_custom_command(TARGET fable_2 POST_BUILD
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_TOOLING_RUNTIME_DIR}/rexruntime.dll" "$<TARGET_FILE_DIR:fable_2>/rexruntime.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_TOOLING_RUNTIME_DIR}/rexgpu-xenos.dll" "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos.dll"
          COMMENT "Staging fresh source-built runtime/plugin (out/tooling/runtime-sdk)")
    endif()
    # Always stage the ReXGlue runtime/plugin PDBs next to the exe (the
    # runtime is built Release + -g, so these carry full symbols) so the
    # debugger/profiler symbolizes ReXGlue code with no manual PDB setup.
    if(EXISTS "${FABLE2_TOOLING_RUNTIME_DIR}/rexruntime.pdb" AND EXISTS "${FABLE2_TOOLING_RUNTIME_DIR}/rexgpu-xenos.pdb")
        add_custom_command(TARGET fable_2 POST_BUILD
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_TOOLING_RUNTIME_DIR}/rexruntime.pdb" "$<TARGET_FILE_DIR:fable_2>/rexruntime.pdb"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_TOOLING_RUNTIME_DIR}/rexgpu-xenos.pdb" "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos.pdb"
          COMMENT "Staging ReXGlue runtime/plugin PDBs (debug symbols)")
    endif()
endif()

# --- Vulkan renderer: Stage 1 smoke test ----------------------------------
# A self-contained Vulkan pipeline (vendored Khronos headers in
# thirdparty/vulkan + a dynamic vulkan-1.dll loader) that creates a window,
# inits Vulkan, and presents a clear color. Proves the Vulkan foundation in
# this repo before Stage 2 builds the Xenos->Vulkan command processor so the
# game itself renders. Standalone: no SDK/rexruntime dependency.
#   build: build.cmd fable_2_vulkan_smoke
if(WIN32)
    # GUI (WIN32) subsystem on purpose: a console-subsystem process is blocked
    # from creating top-level windows in this environment (CreateWindowExW
    # returns NULL with error 0), whereas the game (GUI/SDL) works.
    add_executable(fable_2_vulkan_smoke WIN32 src/vulkan_smoke.cpp)
    target_include_directories(fable_2_vulkan_smoke PRIVATE
      thirdparty/vulkan
      ${FABLE2_SDK_ROOT}/include
    )
    # Use the SDK's UI framework (rex::ui::Window / SDLWindowedAppContext) from
    # the SHARED rexruntime.dll — the game's proven window path. The SDL_*
    # symbols are NOT exported, but the rex::ui::* ones are, so link the import
    # lib and stage the DLL next to the exe.
    target_link_libraries(fable_2_vulkan_smoke PRIVATE
      ${FABLE2_SDK_ROOT}/lib/rexruntime.lib
      user32 gdi32
    )
    add_custom_command(TARGET fable_2_vulkan_smoke POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
              ${FABLE2_SDK_ROOT}/bin/rexruntime.dll
              $<TARGET_FILE_DIR:fable_2_vulkan_smoke>
    )
endif()

# Opt out of the stable-hash PE cleaning below (keeps the linker's real
# timestamp/PDB GUID so a debugger or profiler can match fable_2.pdb by
# UUID/Age). Used by the profiling build (build.cmd -release fable_2_profiler).
option(FABLE2_NO_PE_CLEAN
    "Skip the post-build stable-hash PE cleaning (exports trimming + timestamp/PDB-GUID pinning)" OFF)

# --- Reproducible / stable-hash post-processing (cert-free AV-clean builds) -
# Post-build AV-clean hardening (see plans/av-clean-builds.md +
# tools/repro/clean_pe.py): for the staged release dir it (1) trims the
# rexruntime export table to the curated set -- the union of every symbol a
# consumer actually imports plus the whole rex:: public API -- dropping the
# ~6k std/RTTI/operator/unused-__imp_ symbols WINDOWS_EXPORT_ALL_SYMBOLS leaks
# (an abnormal profile that feeds AV generic heuristics), and (2) pins the
# volatile PE bytes (timestamp, PDB GUID, PDB path). Together they make an
# identical source build to an identical, profile-normal hash that can
# accumulate a benign AV reputation instead of looking like a novel unknown
# file on every rebuild. Idempotent; missing files skipped.
if(WIN32 AND FABLE2_NO_PE_CLEAN)
    message(STATUS "FABLE2_NO_PE_CLEAN=ON: skipping stable-hash PE cleaning (real PDB GUID kept for debuggers/profilers)")
elseif(WIN32)
    find_package(Python3 COMPONENTS Interpreter QUIET)
    if(Python3_FOUND)
        add_custom_command(TARGET fable_2 POST_BUILD
            COMMAND ${Python3_EXECUTABLE}
                    ${CMAKE_CURRENT_SOURCE_DIR}/tools/repro/clean_pe.py
                    $<TARGET_FILE_DIR:fable_2>
                    --quiet
            COMMENT "Trimming exports + normalizing PE (stable, AV-clean build)"
            VERBATIM)
    else()
        message(STATUS "Python3 not found; skipping stable-hash PE cleaning")
    endif()
endif()
