cmake_minimum_required(VERSION 3.20)
project(MyGameRecomp
    VERSION 0.1.0
    LANGUAGES C CXX
    DESCRIPTION "Recompiled PS3 game project (ps3recomp)"
)

# ---------------------------------------------------------------------------
# Standards
# ---------------------------------------------------------------------------
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Prefer Ninja if available
if(NOT CMAKE_GENERATOR MATCHES "Ninja" AND NOT DEFINED ENV{CMAKE_GENERATOR})
    message(STATUS "Tip: use -G Ninja for faster builds")
endif()

# ---------------------------------------------------------------------------
# The two directories this project is built from
# ---------------------------------------------------------------------------
# PS3RECOMP_DIR is the toolkit checkout: include/, runtime/, libs/, tools/.
# RECOMP_DIR is where the lifter's output for THIS game lives. They are
# separate on purpose -- a port keeps its generated code in its own tree and
# points at whatever toolkit checkout it is building against:
#
#   cmake -B build -G Ninja \
#         -DPS3RECOMP_DIR=/path/to/ps3recomp \
#         -DRECOMP_DIR=/path/to/my_game/recompiled
#
if(NOT DEFINED PS3RECOMP_DIR)
    # Default: this project is still sitting inside ps3recomp/templates/project/
    get_filename_component(PS3RECOMP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../" ABSOLUTE)
endif()
get_filename_component(PS3RECOMP_DIR "${PS3RECOMP_DIR}" ABSOLUTE)

set(RECOMP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/recompiled" CACHE PATH
    "Directory holding the lifter's output: ppu_recomp.h and the lifted sources")
get_filename_component(RECOMP_DIR "${RECOMP_DIR}" ABSOLUTE)

message(STATUS "ps3recomp SDK:   ${PS3RECOMP_DIR}")
message(STATUS "Lifted output:   ${RECOMP_DIR}")

if(NOT EXISTS "${RECOMP_DIR}/ppu_recomp.h")
    message(FATAL_ERROR
        "No ppu_recomp.h in ${RECOMP_DIR}.\n"
        "That header and the lifted sources beside it are what "
        "tools/ppu_lifter.py writes. Run the lifter, drop its output there, "
        "or point -DRECOMP_DIR= at wherever it already is.")
endif()

# The lifted translation units. A game's lift is hundreds of generated files,
# so they are globbed rather than listed; re-run CMake after a fresh lift.
file(GLOB RECOMP_SOURCES CONFIGURE_DEPENDS
    "${RECOMP_DIR}/*.c"
    "${RECOMP_DIR}/*.cpp"
)
if(NOT RECOMP_SOURCES)
    message(FATAL_ERROR
        "${RECOMP_DIR} has a ppu_recomp.h but no .c or .cpp beside it. The "
        "lifted functions and the function table live in those; without them "
        "there is nothing to run.")
endif()
# PORT_HLE_NIDS_GENERATE: the table kept beside the lift must not be compiled too (duplicate ppu_hle_register_all).
if(PORT_HLE_NIDS_GENERATE)
    list(FILTER RECOMP_SOURCES EXCLUDE REGEX "[/\\]ppu_hle_nids\\.cpp$")
endif()
list(LENGTH RECOMP_SOURCES RECOMP_SOURCE_COUNT)
message(STATUS "Lifted sources:  ${RECOMP_SOURCE_COUNT} file(s)")

# ---------------------------------------------------------------------------
# ps3recomp runtime library
# ---------------------------------------------------------------------------
# Built from source as a subdirectory rather than imported as a prebuilt
# archive. A static library carries none of its own link requirements, so an
# imported one arrives without SDL2, pthreads, the Metal frameworks or the
# Winsock/XInput imports -- the link then fails on every platform for a
# different reason. add_subdirectory hands all of that over with the target.
# T-452 (PLANO_PC_FRACO 2b): profile-guided optimization and ThinLTO over everything -- the lifted PPU and SPU
# code, the runtime and the port. Set before the runtime subdirectory so it inherits them; the shared PPU object
# cache keys on the directory options, so instrumented/optimized objects never mix with the plain ones.
#   PORT_PGO=gen  instrumented build; the run writes LLVM_PROFILE_FILE after PORT_PGO_DUMP_S seconds (main.cpp)
#   PORT_PGO=use  optimize with PORT_PGO_PROFDATA (llvm-profdata merge of those runs; name it by its content)
#   PORT_LTO=ON   ThinLTO, cache in <build>/lto_cache
set(PORT_PGO "" CACHE STRING "PGO: empty = off, gen = instrumented, use = optimize with PORT_PGO_PROFDATA")
set(PORT_PGO_PROFDATA "" CACHE FILEPATH "Merged .profdata for PORT_PGO=use")
option(PORT_LTO "ThinLTO over the whole executable (clang-cl + lld-link)" OFF)
if(PORT_PGO STREQUAL "gen")
    add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-fprofile-generate>)
    add_compile_definitions(PORT_PGO_GEN=1)
    get_filename_component(_llvm_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
    file(GLOB _clang_rt_dir "${_llvm_bin}/../lib/clang/*/lib/windows")
    add_link_options("/LIBPATH:${_clang_rt_dir}" clang_rt.profile-x86_64.lib)
    message(STATUS "PGO:             instrumented (${_clang_rt_dir})")
elseif(PORT_PGO STREQUAL "use")
    if(NOT EXISTS "${PORT_PGO_PROFDATA}")
        message(FATAL_ERROR "PORT_PGO=use needs PORT_PGO_PROFDATA (got '${PORT_PGO_PROFDATA}')")
    endif()
    file(SHA256 "${PORT_PGO_PROFDATA}" _pgo_sha)
    string(SUBSTRING "${_pgo_sha}" 0 16 _pgo_sha)
    # the -D carries the profile's content into the PPU cache key (its path alone would not)
    add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:-fprofile-use=${PORT_PGO_PROFDATA};-Wno-profile-instr-unprofiled;-Wno-profile-instr-out-of-date;-Wno-backend-plugin>")
    add_compile_definitions(PORT_PGO_PROFILE=0x${_pgo_sha})
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PORT_PGO_PROFDATA}")
    message(STATUS "PGO:             use ${PORT_PGO_PROFDATA} (${_pgo_sha})")
elseif(PORT_PGO)
    message(FATAL_ERROR "PORT_PGO must be empty, gen or use (got '${PORT_PGO}')")
endif()
if(PORT_LTO)
    add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:-flto=thin;-fuse-ld=lld>")   # clang-cl wants -fuse-ld=lld even with /c
    add_link_options("/lldltocache:${CMAKE_BINARY_DIR}/lto_cache" /opt:lldltojobs=4)
    message(STATUS "LTO:             thin")
endif()

# PORT_SPU_LEAN (T-457) is ON by default since v0.4.0 (-DPORT_SPU_LEAN=OFF for a build that debugs with the LS
# diagnostics). Declared here, before its first use, so a fresh configure also gets the TLS line below.
option(PORT_SPU_LEAN "Lifted SPU code: SSE4.1 helpers without the run-time switch, no /GS (T-457)" ON)
# PORT_SPU_FMODE_FIXED (T-484, needs PORT_SPU_LEAN): the lifted code's float model is fixed at the defaults every
# path uses (SPU_XFLOAT=2 exact model, SPU_RZ=1 MXCSR round-toward-zero; FMA3 stays a run-time choice), so each float op
# drops the dead paths and their tests. SPU_XFLOAT/SPU_RZ in the environment then reach only the runtime and the
# interpreter; -DPORT_SPU_FMODE_FIXED=OFF brings them back for the lifted code (diagnostics). Replay 1.12, 2012 ruler:
# Edge +1.5/+2.9%, SCREAM PM +3.8/+3.1%, same bits (work/lifter/t484).
option(PORT_SPU_FMODE_FIXED "Lifted SPU code: float model fixed at SPU_XFLOAT=2 + SPU_RZ=1 (T-484)" ON)
# PORT_SPU_LEAN also drops clang-cl's on-demand TLS guard from C code: every
# __declspec(thread) access (g_spu_trampoline_fn on every lifted transfer, the drain loop's per-hop
# globals) otherwise reloads the TLS block and tests __tls_guard first. C has no dynamic thread-local
# initialisers, so the guard never does anything there: same code, fewer loads per hop.
if(PORT_SPU_LEAN AND MSVC AND CMAKE_C_COMPILER_ID MATCHES "Clang")
    add_compile_options($<$<COMPILE_LANGUAGE:C>:/Zc:tlsGuards->)
    message(STATUS "SPU lean:        C without TLS guards")
endif()
add_subdirectory("${PS3RECOMP_DIR}" "${CMAKE_BINARY_DIR}/ps3recomp_sdk"
                 EXCLUDE_FROM_ALL)

# ---------------------------------------------------------------------------
# The HLE NID table
# ---------------------------------------------------------------------------
# Maps each firmware import NID onto the handler in the runtime that answers
# it. A port may keep a hand-curated one beside its lift; otherwise generate
# the canonical every-module table, which is what the toolkit's own boot
# harness uses.
# PORT_HLE_NIDS_GENERATE=ON (T-259): ignore a table kept beside the lift and generate it from the runtime being
# built, so every HLE function the runtime now implements is registered (the 1.00 lift's table dates from 19/10).
option(PORT_HLE_NIDS_GENERATE "Generate the HLE NID table from the runtime instead of RECOMP_DIR/ppu_hle_nids.cpp" OFF)
if(EXISTS "${RECOMP_DIR}/ppu_hle_nids.cpp" AND NOT PORT_HLE_NIDS_GENERATE)
    set(HLE_NID_TABLE "${RECOMP_DIR}/ppu_hle_nids.cpp")
    message(STATUS "HLE NID table:   ${HLE_NID_TABLE}")
else()
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    set(HLE_NID_TABLE "${CMAKE_CURRENT_BINARY_DIR}/ppu_hle_nids.cpp")
    add_custom_command(
        OUTPUT "${HLE_NID_TABLE}"
        COMMAND "${Python3_EXECUTABLE}"
                "${PS3RECOMP_DIR}/tools/gen_hle_nids.py"
                --out "${HLE_NID_TABLE}" --all
        DEPENDS "${PS3RECOMP_DIR}/tools/gen_hle_nids.py"
                "${PS3RECOMP_DIR}/tools/nid_database.py"
        COMMENT "Generating the HLE NID -> handler table")
    message(STATUS "HLE NID table:   generated (every module)")
endif()

# ---------------------------------------------------------------------------
# The game executable
# ---------------------------------------------------------------------------
# The PPU boot scaffold is compiled here rather than taken from the runtime
# library: every one of these files #includes the lifter's per-game
# ppu_recomp.h, so they can only be built against a particular lift.
set(PS3RECOMP_SCAFFOLD
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_loader.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_hle.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_fs.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_sysprx.cpp"
)
# Cached getenv (T-164) for the PPU scaffold and the port's own sources (their
# _putenv defaults must refresh the cache); the lifted PPU/SPU translation units
# are left alone (no rebuild of the lift).
if(PORT_GETENV_FI AND PORT_GETENV_HEADER)
    if(MSVC)
        set(_pge_fi "/FI${PORT_GETENV_HEADER}")
    else()
        set(_pge_fi "-include;${PORT_GETENV_HEADER}")
    endif()
    set_source_files_properties(${PS3RECOMP_SCAFFOLD} main.cpp stubs.cpp game_hacks.cpp interp.cpp salas.cpp prof.cpp
                                PROPERTIES COMPILE_OPTIONS "${_pge_fi}")
endif()

# ---------------------------------------------------------------------------
# Lifted SPU images (optional)
# ---------------------------------------------------------------------------
# A title that runs SPU work gets its images lifted into <SPU_DIR>/<image>/ by
# tools/spu_lifter.py, with a registration TU generated by
# tools/build_spu_workloads.py. cellSpurs dispatches them by content
# fingerprint, so the generated table is keyed to the images THIS build ships:
# a table generated from another release registers signatures that can never
# match, which is worse than shipping none. Both the table and the job-module
# registry self-register from constructors and are therefore optional -- a tree
# that has not lifted its SPU images yet configures and links without them.
set(SPU_DIR "${CMAKE_CURRENT_SOURCE_DIR}/spu" CACHE PATH
    "Directory holding the lifted SPU images (<SPU_DIR>/<image>/spu_recomp.c)")
option(PORT_ENABLE_SPU "Compile and register the title's lifted SPU images" ON)

set(SPU_SOURCES "")
if(PORT_ENABLE_SPU)
    file(GLOB SPU_LIFTED CONFIGURE_DEPENDS "${SPU_DIR}/*/spu_recomp.c")
    if(SPU_LIFTED)
        list(LENGTH SPU_LIFTED SPU_IMAGE_COUNT)
        message(STATUS "Lifted SPU:      ${SPU_IMAGE_COUNT} image(s)")
        # The musttail indirect-branch fast path. Excluded from the runtime
        # library on purpose (see the SDK root CMakeLists): it must be built
        # here, by clang, or the tail call degrades to a plain call and a guest
        # loop that branches indirectly walks the thread stack off its guard
        # page a few thousand iterations in.
        list(APPEND SPU_SOURCES ${SPU_LIFTED}
             "${PS3RECOMP_DIR}/runtime/spu/spu_dispatch_mt.c")
        # Generated beside the lift; each is present only once its generator
        # has been run, so each is included only if it is actually here.
        foreach(gen IN ITEMS spu_workloads.c jobmods_register.c taskset_policy_bytes.c)
            if(EXISTS "${SPU_DIR}/${gen}")
                list(APPEND SPU_SOURCES "${SPU_DIR}/${gen}")
            else()
                message(STATUS "  (no ${gen} -- not registered)")
            endif()
        endforeach()
    else()
        message(STATUS "Lifted SPU:      none found in ${SPU_DIR}")
    endif()
    # Further lift trees kept apart from SPU_DIR (each <dir>/<image>/spu_recomp.c
    # plus any <dir>/*_register.c). Default: a sibling "lifted_rt" when present.
    set(SPU_EXTRA_DIRS "" CACHE STRING
        "Extra lifted-SPU directories (;-separated); empty = <SPU_DIR>/../lifted_rt if it exists")
    set(_spu_extra ${SPU_EXTRA_DIRS})
    if(NOT _spu_extra AND EXISTS "${SPU_DIR}/../lifted_rt")
        get_filename_component(_spu_extra "${SPU_DIR}/../lifted_rt" ABSOLUTE)
    endif()
    foreach(xd IN LISTS _spu_extra)
        file(GLOB _xl CONFIGURE_DEPENDS "${xd}/*/spu_recomp.c")
        file(GLOB _xr CONFIGURE_DEPENDS "${xd}/*_register.c")
        list(LENGTH _xl _xn)
        message(STATUS "Lifted SPU (extra): ${_xn} image(s) in ${xd}")
        list(APPEND SPU_SOURCES ${_xl} ${_xr})
        if(_xl AND NOT SPU_LIFTED)
            list(APPEND SPU_SOURCES "${PS3RECOMP_DIR}/runtime/spu/spu_dispatch_mt.c")
        endif()
    endforeach()
    # T-446: region twins of the hot lifted SPU loops (spu_twins.list), generated from the lift into the build tree by
    # the SDK's tools/spu_region_twin.py, so they always match the lifted code. They run only with SPU_TWIN=1.
    option(PORT_SPU_TWINS "Build the region twins of the hot lifted SPU loops (they run with SPU_TWIN=1)" ON)
    set(_tw_py "${PS3RECOMP_DIR}/tools/spu_region_twin.py")
    if(PORT_SPU_TWINS AND _spu_extra AND EXISTS "${_tw_py}" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/spu_twins.list")
        find_package(Python3 COMPONENTS Interpreter REQUIRED)
        set(_tw_out "${CMAKE_CURRENT_BINARY_DIR}/spu_twins")
        set(_tw_dep "")
        set(_tw_args "")
        foreach(xd IN LISTS _spu_extra)
            file(GLOB _t CONFIGURE_DEPENDS "${xd}/*/spu_recomp.c")
            list(APPEND _tw_dep ${_t})
            list(APPEND _tw_args --lifted "${xd}")
        endforeach()
        add_custom_command(OUTPUT "${_tw_out}/spu_region_twins.c"
            COMMAND "${Python3_EXECUTABLE}" "${_tw_py}" ${_tw_args}
                    --roots "${CMAKE_CURRENT_SOURCE_DIR}/spu_twins.list" --out "${_tw_out}"
            DEPENDS ${_tw_dep} "${_tw_py}" "${CMAKE_CURRENT_SOURCE_DIR}/spu_twins.list"
            COMMENT "SPU region twins (T-446)" VERBATIM)
        list(APPEND SPU_SOURCES "${_tw_out}/spu_region_twins.c")
        message(STATUS "SPU region twins: ${_tw_out}/spu_region_twins.c (SPU_TWIN=1 to run them)")
    endif()
endif()

# The lifted PPU translation units are the slow part of every build (minutes
# of CPU each) and identical in every build tree that points at the same lift,
# so PORT_PPU_CACHE (see "Shared PPU object cache" at the end) compiles each one
# once and shares the object. The HLE NID table stays with the executable: it
# is tiny and names the runtime's handlers.
option(PORT_PPU_CACHE "Share compiled lifted-PPU objects between build trees (content-addressed)" ON)
set(PPU_CACHED_SOURCES ${RECOMP_SOURCES})
list(REMOVE_ITEM PPU_CACHED_SOURCES "${HLE_NID_TABLE}")
set(RECOMP_EXE_SOURCES ${RECOMP_SOURCES})
if(PORT_PPU_CACHE)
    list(REMOVE_ITEM RECOMP_EXE_SOURCES ${PPU_CACHED_SOURCES})
endif()

add_executable(${PROJECT_NAME}
    main.cpp
    stubs.cpp
    cfx_precompile.cpp
    game_hacks.cpp
    interp.cpp
    salas.cpp
    prof.cpp
    ${PS3RECOMP_SCAFFOLD}
    ${HLE_NID_TABLE}
    ${RECOMP_EXE_SOURCES}
    ${SPU_SOURCES}
)

# The lifted SPU sources use bare #include "spu_context.h". Scope that include
# directory (and their flags) to those TUs alone: adding it to the whole target
# changes the compile command of every lifted PPU TU, and those are large enough
# that the needless rebuild is measured in tens of minutes.
#   SPU_USE_DISPATCH_MT -- route SPU_IB_DISPATCH through the musttail path.
#   -O2                 -- the target-wide -O1 exists for the huge PPU TUs; the
#                          SPU sources are small and decode-hot, so they get
#                          full optimization. The last -O on the line wins.
option(PORT_SPU_SSE41 "Build the lifted SPU code with -msse4.1 (T-198)" ON)
if(PORT_SPU_SSE41)
    set_property(SOURCE main.cpp APPEND PROPERTY COMPILE_DEFINITIONS PORT_SPU_SSE41_BUILD=1)
endif()
if(SPU_SOURCES)
    set_source_files_properties(${SPU_SOURCES} PROPERTIES
        INCLUDE_DIRECTORIES "${PS3RECOMP_DIR}/runtime/spu"
        COMPILE_DEFINITIONS SPU_USE_DISPATCH_MT)
    if(NOT MSVC OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
        # PORT_SPU_SSE41 (T-198): the lifted SPU code may use SSE4.1 directly, so the
        # SSE4.1 helpers (shufb, byte tables) inline instead of being called. main()
        # refuses to start on a CPU without SSE4.1 (PORT_SPU_SSE41_BUILD).
        if(PORT_SPU_SSE41)
            set_source_files_properties(${SPU_SOURCES} PROPERTIES COMPILE_OPTIONS "-O2;-msse4.1")
            # PORT_SPU_LEAN (T-457): the lifted code always takes the SSE4.1 helper path (SPU_SIMD_FORCE: no
            # g_spu_simd load + branch and no cold byte loop per op; main() already requires SSE4.1) and has no
            # /GS stack cookie in its small per-block functions. Same bits; img 1's hot loop -6% per hop offline.
            # SPU_LS_NODIAG: no LS diagnostics in the lifted code (SPU_LS_WATCH, SPU_LS_LOWREAD, SPU_SMC_WATCH and
            # SPU_WWS_PROBE do nothing there; use a build without LEAN to debug with them): no flag load + branch
            # per LS access. Same bits; +4.7-5.0% offline on img 1, Edge and the SCREAM PM (T-457).
            if(PORT_SPU_LEAN)
                set(_spu_lean_defs "SPU_USE_DISPATCH_MT;SPU_SIMD_FORCE;SPU_LS_NODIAG")
                if(PORT_SPU_FMODE_FIXED)
                    list(APPEND _spu_lean_defs SPU_FMODE_FIXED)
                    message(STATUS "SPU float:       lifted code fixed at SPU_XFLOAT=2 + SPU_RZ=1 (PORT_SPU_FMODE_FIXED)")
                endif()
                set_source_files_properties(${SPU_SOURCES} PROPERTIES COMPILE_DEFINITIONS "${_spu_lean_defs}")
                if(MSVC)
                    set_source_files_properties(${SPU_SOURCES} PROPERTIES COMPILE_OPTIONS "-O2;-msse4.1;/GS-")
                endif()
            endif()
        else()
            set_source_files_properties(${SPU_SOURCES} PROPERTIES COMPILE_OPTIONS "-O2")
        endif()
    endif()
endif()

# Keyboard map read by cellPad next to the .exe. Copied once, never over a
# copy the user has already edited.
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/keymap.ini" AND
   NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/keymap.ini")
    file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/keymap.ini" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
endif()

# app.rc carries the icon, the version block and the manifest, and needs a
# resource compiler. Listing it as a source on a host that has none is not a
# link error, it is a "no rule to make" for a file nothing can consume.
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/app.rc")
    target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/app.rc")
endif()

# T-285 (20/10): the executable is PSASBR.exe (no spaces; scripts/game_exe_names.txt lists it for the
# watchers); the game's full name is in app.rc. PORT_ICON_PNG = the user's PS3_GAME/ICON0.PNG: if given, a square
# icon is generated into the build folder at configure time (tools/png_to_ico.py) and linked in; the icon bytes
# come from the user's copy of the game, never from this tree.
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME PSASBR)
set(PORT_ICON_PNG "" CACHE FILEPATH "The user's PS3_GAME/ICON0.PNG, for the executable's icon (optional)")
if(WIN32 AND PORT_ICON_PNG AND EXISTS "${PORT_ICON_PNG}")
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    execute_process(COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/png_to_ico.py"
                            "${PORT_ICON_PNG}" "${CMAKE_CURRENT_BINARY_DIR}/port_icon.ico" RESULT_VARIABLE _ico_rc)
    if(_ico_rc EQUAL 0)
        file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/port_icon.rc" "1 ICON \"port_icon.ico\"
")
        target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/port_icon.rc")
        message(STATUS "Executable icon: from ${PORT_ICON_PNG}")
    else()
        message(WARNING "PORT_ICON_PNG: could not make an icon from ${PORT_ICON_PNG}; building without one")
    endif()
endif()

target_include_directories(${PROJECT_NAME} PRIVATE
    "${RECOMP_DIR}"                            # the generated ppu_recomp.h
    "${PS3RECOMP_DIR}/include"
    "${PS3RECOMP_DIR}/runtime/ppu"
    "${PS3RECOMP_DIR}/runtime/spu"
    "${PS3RECOMP_DIR}/runtime/memory"
    "${PS3RECOMP_DIR}/runtime/syscalls"
    "${PS3RECOMP_DIR}/runtime/platform"        # win32_compat.h and friends
    "${PS3RECOMP_DIR}/libs/video"
    "${CMAKE_CURRENT_SOURCE_DIR}"
)

target_link_libraries(${PROJECT_NAME} PRIVATE
    ps3recomp_runtime
)

# The runtime is C++, and on Apple also Objective-C. Letting CMake infer the
# link language from the generated C would leave the C++ standard library out.
set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX)

# Windows system import libraries. dbghelp symbolizes crash-handler addresses
# against the PDB, user32 and gdi32 are what the backend's window and its GDI
# present are made of, and winmm is timeBeginPeriod. None of the four names
# anything a POSIX linker can find; the D3D12 backend's own imports come in
# through the #pragma comment(lib) in libs/video/rsx_d3d12_backend.c.
if(WIN32)
    target_link_libraries(${PROJECT_NAME} PRIVATE
        dbghelp
        user32
        gdi32
        winmm
        d3d12
        dxgi
        dxguid
        d3dcompiler
    )
endif()

# T-570: PSASBR-shaders.exe -- the --precompile-shaders / --shader-source-id path of the game exe on its own: the same
# runtime library and CFX/psarc reader, none of the lifted game code (~1 MB, links in seconds). The launcher runs it
# while the build tools download, so the shaders are ready when the game exe is. Same translator id and same cache
# as PSASBR.exe (shader_tool.cpp). PORT_SHADER_TOOL=OFF leaves it out.
option(PORT_SHADER_TOOL "Also build PSASBR-shaders.exe, the stand-alone shader preparer (T-570)" ON)
if(WIN32 AND PORT_SHADER_TOOL)
    add_executable(port_shader_tool
        shader_tool.cpp
        stubs.cpp
        cfx_precompile.cpp
        "${PS3RECOMP_DIR}/runtime/host/host_stubs.c"
    )
    get_target_property(_shc_tool_inc ${PROJECT_NAME} INCLUDE_DIRECTORIES)
    target_include_directories(port_shader_tool PRIVATE ${_shc_tool_inc})
    target_link_libraries(port_shader_tool PRIVATE ps3recomp_runtime
        dbghelp user32 gdi32 winmm d3d12 dxgi dxguid d3dcompiler bcrypt)
    set_target_properties(port_shader_tool PROPERTIES OUTPUT_NAME PSASBR-shaders LINKER_LANGUAGE CXX)
    target_compile_definitions(port_shader_tool PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()

# ---------------------------------------------------------------------------
# Compiler-specific flags
# ---------------------------------------------------------------------------
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
    target_compile_options(${PROJECT_NAME} PRIVATE
        -Wall -Wextra -Wno-unused-parameter
        -fno-strict-aliasing
    )
elseif(MSVC)
    target_compile_options(${PROJECT_NAME} PRIVATE
        /W3 /wd4100 /wd4244 /wd4267
        /Zc:__cplusplus         # report correct __cplusplus value
        # ppu_recomp.h defines PPU_FENCE, the lowering of the guest's
        # sync/lwsync/eieio/isync, and in C that is <stdatomic.h>. The lifter
        # writes C++ by default, but --single-file writes ppu_recomp.c, and a
        # port that took it would not compile without this. The runtime's own
        # build already carries the same flag for the same reason.
        /experimental:c11atomics
    )
    target_compile_definitions(${PROJECT_NAME} PRIVATE
        _CRT_SECURE_NO_WARNINGS
    )
endif()

# The lifted translation units are machine-generated and can run to hundreds of
# thousands of lines each, so they get their own flags:
#
#   - warnings off. They are about generated code and nobody will act on them,
#     and at this volume they bury the diagnostics that matter.
#   - no floating-point contraction. arm64 fuses a multiply and an add by
#     default; a PPC fmadd is spelt as one where the lifter means one.
#   - /bigobj on MSVC. A single lifted file routinely passes the 65,536-section
#     limit of the default object format.
if(MSVC)
    set_source_files_properties(${RECOMP_SOURCES} PROPERTIES
        COMPILE_OPTIONS "/w;/bigobj")
else()
    set_source_files_properties(${RECOMP_SOURCES} PROPERTIES
        COMPILE_OPTIONS "-w;-ffp-contract=off")
endif()

# Guest memory access in the lifted code. Every load and store there was a call
# into the runtime's instrumented vm_readN/vm_writeN; PORT_FASTMEM force-includes
# runtime/ppu/ppu_fastmem.h, which turns plain-memory accesses into an inline
# byte-swapped load or store on vm_base and calls the runtime only for raw SPU
# registers, the GCM control page and lines an SPU has reserved. Setting
# PPU_FASTMEM=0 (or any memory diagnostic such as PPU_RWATCH/PPU_WWATCH) at run
# time sends every access back through the instrumented accessors.
option(PORT_FASTMEM "Inline plain guest memory access in the lifted PPU code" ON)
if(PORT_FASTMEM)
    set(_fastmem_h "${PS3RECOMP_DIR}/runtime/ppu/ppu_fastmem.h")
    if(MSVC)
        set_property(SOURCE ${RECOMP_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "/FI${_fastmem_h}")
    else()
        set_property(SOURCE ${RECOMP_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "-include;${_fastmem_h}")
    endif()
    message(STATUS "PPU fastmem:     on")
endif()

# Optimization level for the lifted PPU TUs only, e.g. -DPORT_PPU_OPT=-O2.
# Empty keeps the target-wide level. As with the SPU sources, the last -O on
# the command line wins.
set(PORT_PPU_OPT "" CACHE STRING "Extra optimization flag for the lifted PPU sources (e.g. -O2)")
if(PORT_PPU_OPT)
    set_property(SOURCE ${RECOMP_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "${PORT_PPU_OPT}")
    message(STATUS "PPU opt:         ${PORT_PPU_OPT}")
endif()

# ---------------------------------------------------------------------------
# Shared PPU object cache
# ---------------------------------------------------------------------------
# Every build tree of this game that points at the same lift compiles the same
# lifted PPU sources with the same flags into the same objects -- the bulk of a
# from-scratch build. With PORT_PPU_CACHE each object is stored once in
# PORT_PPU_CACHE_DIR under a key made of the source's content hash and
# everything else that decides its bytes (compiler and version, configuration,
# flags, ppu_recomp.h, ppu_fastmem.h). A build that finds its key links the
# stored object; one that does not compiles the source as usual and stores the
# object for the next tree. Any change to those inputs re-runs CMake (they are
# configure dependencies), which recomputes the keys, so a stale object is
# never linked. The lifted sources include nothing but ppu_recomp.h, the
# force-included ppu_fastmem.h and the C library; anything they come to depend
# on besides that has to go into the key below. Bump the recipe tag when the
# way these objects are built changes in a way the key does not see.
if(PORT_PPU_CACHE)
    set(PORT_PPU_CACHE_DIR "${RECOMP_DIR}/../cache/ppu" CACHE PATH
        "Where compiled lifted-PPU objects are shared between build trees")
    get_filename_component(_pc_dir "${PORT_PPU_CACHE_DIR}" ABSOLUTE)
    file(MAKE_DIRECTORY "${_pc_dir}")

    string(TOUPPER "${CMAKE_BUILD_TYPE}" _pc_cfg)
    get_target_property(_pc_topts ${PROJECT_NAME} COMPILE_OPTIONS)
    get_target_property(_pc_tdefs ${PROJECT_NAME} COMPILE_DEFINITIONS)
    get_target_property(_pc_iopts ps3recomp_runtime INTERFACE_COMPILE_OPTIONS)
    get_target_property(_pc_idefs ps3recomp_runtime INTERFACE_COMPILE_DEFINITIONS)
    get_directory_property(_pc_dopts COMPILE_OPTIONS)
    get_directory_property(_pc_ddefs COMPILE_DEFINITIONS)
    list(GET PPU_CACHED_SOURCES 0 _pc_first)
    get_source_file_property(_pc_sopts "${_pc_first}" COMPILE_OPTIONS)
    foreach(v _pc_topts _pc_tdefs _pc_iopts _pc_idefs _pc_dopts _pc_ddefs _pc_sopts)
        if(NOT ${v})
            set(${v} "")
        endif()
    endforeach()
    file(SHA256 "${RECOMP_DIR}/ppu_recomp.h" _pc_hdr)
    set(_pc_deps "${RECOMP_DIR}/ppu_recomp.h")
    set(_pc_fm "off")
    if(PORT_FASTMEM)
        file(SHA256 "${_fastmem_h}" _pc_fm)
        list(APPEND _pc_deps "${_fastmem_h}")
    endif()
    set(_pc_recipe
        "recipe=1"
        "cxx=${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}"
        "c=${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}"
        "config=${CMAKE_BUILD_TYPE}"
        "cxxflags=${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${_pc_cfg}}"
        "cflags=${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${_pc_cfg}}"
        "msvcrt=${CMAKE_MSVC_RUNTIME_LIBRARY}"
        "std=${CMAKE_CXX_STANDARD}/${CMAKE_C_STANDARD}"
        "target=${_pc_topts} ${_pc_tdefs}"
        "runtime=${_pc_iopts} ${_pc_idefs}"
        "dir=${_pc_dopts} ${_pc_ddefs}"
        "source=${_pc_sopts}"
        "ppu_recomp.h=${_pc_hdr}"
        "ppu_fastmem.h=${_pc_fm}")
    # Paths name the checkout, not the content: two agents' trees with the same
    # ppu_fastmem.h must land on the same key.
    string(REPLACE "${PS3RECOMP_DIR}" "<sdk>" _pc_recipe "${_pc_recipe}")
    string(REPLACE "${RECOMP_DIR}" "<lift>" _pc_recipe "${_pc_recipe}")
    string(REPLACE ";" "\n" _pc_recipe_txt "${_pc_recipe}")
    file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/ppu_cache_recipe.txt" "${_pc_recipe_txt}\n")
    string(SHA256 _pc_common "${_pc_recipe_txt}")

    set(_pc_hits "")
    set(_pc_miss "")
    set(_pc_map "")
    foreach(src IN LISTS PPU_CACHED_SOURCES)
        file(SHA256 "${src}" _pc_src)
        string(SHA256 _pc_key "${_pc_common}|${_pc_src}")
        string(SUBSTRING "${_pc_key}" 0 32 _pc_key)
        get_filename_component(_pc_name "${src}" NAME)
        get_filename_component(_pc_stem "${src}" NAME_WE)
        set(_pc_obj "${_pc_dir}/${_pc_stem}-${_pc_key}${CMAKE_CXX_OUTPUT_EXTENSION}")
        if(EXISTS "${_pc_obj}")
            list(APPEND _pc_hits "${_pc_obj}")
        else()
            list(APPEND _pc_miss "${src}")
            string(APPEND _pc_map "${_pc_name}|${_pc_obj}\n")
        endif()
    endforeach()
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
        ${PPU_CACHED_SOURCES} ${_pc_deps})

    list(LENGTH _pc_hits _pc_nhit)
    list(LENGTH _pc_miss _pc_nmiss)
    message(STATUS "PPU cache:       ${_pc_nhit} shared, ${_pc_nmiss} to compile (${_pc_dir})")

    if(_pc_hits)
        set_source_files_properties(${_pc_hits} PROPERTIES
            EXTERNAL_OBJECT TRUE GENERATED FALSE)
        target_sources(${PROJECT_NAME} PRIVATE ${_pc_hits})
    endif()
    if(_pc_miss)
        # Compiled exactly as they would be inside the executable: same
        # include path, options and definitions (the per-source properties set
        # above apply here too, being properties of the source files).
        add_library(port_ppu_lifted OBJECT ${_pc_miss})
        target_include_directories(port_ppu_lifted PRIVATE
            $<TARGET_PROPERTY:${PROJECT_NAME},INCLUDE_DIRECTORIES>)
        target_compile_options(port_ppu_lifted PRIVATE
            $<TARGET_PROPERTY:${PROJECT_NAME},COMPILE_OPTIONS>)
        target_compile_definitions(port_ppu_lifted PRIVATE
            $<TARGET_PROPERTY:${PROJECT_NAME},COMPILE_DEFINITIONS>)
        target_sources(${PROJECT_NAME} PRIVATE $<TARGET_OBJECTS:port_ppu_lifted>)

        # Store each fresh object for the other trees (first writer wins; an
        # object already there is left alone).
        file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/ppu_cache_map.txt" "${_pc_map}")
        file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/ppu_cache_objs.txt"
             CONTENT "$<JOIN:$<TARGET_OBJECTS:port_ppu_lifted>,\n>\n")
        add_custom_target(port_ppu_publish ALL
            COMMAND "${CMAKE_COMMAND}"
                    "-DOBJS_FILE=${CMAKE_CURRENT_BINARY_DIR}/ppu_cache_objs.txt"
                    "-DMAP_FILE=${CMAKE_CURRENT_BINARY_DIR}/ppu_cache_map.txt"
                    -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ppu_cache_publish.cmake"
            COMMENT "Storing lifted-PPU objects in the shared cache"
            VERBATIM)
        add_dependencies(port_ppu_publish port_ppu_lifted)
    endif()
endif()

# ---------------------------------------------------------------------------
# Install
# ---------------------------------------------------------------------------
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
