cmake_minimum_required(VERSION 3.20)
project(PSPRecomp VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

include(cmake/BuildLock.cmake)
include(cmake/Sanitizers.cmake)
include(cmake/Coverage.cmake)

# A compiler cache turns a lost .ninja_deps, a clean or a regenerated corpus
# into seconds of cache hits instead of a full rebuild of the generated units.
option(PSPRECOMP_CCACHE "Compile through ccache when it is installed" ON)
if(PSPRECOMP_CCACHE AND NOT CMAKE_CXX_COMPILER_LAUNCHER)
    find_program(PSPRECOMP_CCACHE_PROGRAM ccache)
    if(PSPRECOMP_CCACHE_PROGRAM)
        set(CMAKE_CXX_COMPILER_LAUNCHER "${PSPRECOMP_CCACHE_PROGRAM}")
        # Hash paths inside the checkout relative to it, so checkouts at other
        # paths share cache entries (ccache 4.8+ reads settings given this way).
        execute_process(COMMAND "${PSPRECOMP_CCACHE_PROGRAM}" --version
            OUTPUT_VARIABLE _psprecomp_ccache_version ERROR_QUIET)
        if(_psprecomp_ccache_version MATCHES "version ([0-9]+\\.[0-9]+)"
           AND CMAKE_MATCH_1 VERSION_GREATER_EQUAL "4.8")
            list(APPEND CMAKE_CXX_COMPILER_LAUNCHER "base_dir=${PROJECT_SOURCE_DIR}")
        endif()
        message(STATUS "Compiling through ${PSPRECOMP_CCACHE_PROGRAM}")
    endif()
endif()

# Each generated AOT unit takes well over a gigabyte to compile. Ninja runs the
# compiles of targets in this pool at most this many at a time, whatever -j
# the build was started with. The default allows one per 4 GiB of memory.
cmake_host_system_information(RESULT _psprecomp_memory_mib QUERY TOTAL_PHYSICAL_MEMORY)
math(EXPR _psprecomp_default_jobs "${_psprecomp_memory_mib} / 4096")
if(_psprecomp_default_jobs LESS 1)
    set(_psprecomp_default_jobs 1)
endif()
set(PSPRECOMP_GENERATED_JOBS "${_psprecomp_default_jobs}" CACHE STRING
    "Maximum concurrent compiles of generated AOT code (Ninja generators only)")
if(NOT PSPRECOMP_GENERATED_JOBS MATCHES "^[1-9][0-9]*$")
    message(FATAL_ERROR "PSPRECOMP_GENERATED_JOBS must be a positive integer")
endif()
set_property(GLOBAL APPEND PROPERTY JOB_POOLS psprecomp_generated=${PSPRECOMP_GENERATED_JOBS})

option(PSPRECOMP_BUILD_TESTS "Build reusable PSPRecomp tests" ON)
option(PSPRECOMP_BUILD_PROFILE_TESTS "Build tests supplied by the selected profile" ON)
set(PSPRECOMP_PROFILE "" CACHE STRING "Profile to build from profiles/<name>; empty builds only the framework")

set(PSPRECOMP_GENERATED_OPT_LEVEL "2" CACHE STRING
    "Optimization level for generated AOT units (0, 1, 2 or 3)")
set_property(CACHE PSPRECOMP_GENERATED_OPT_LEVEL PROPERTY STRINGS 0 1 2 3)
if(NOT PSPRECOMP_GENERATED_OPT_LEVEL MATCHES "^[0-3]$")
    message(FATAL_ERROR "PSPRECOMP_GENERATED_OPT_LEVEL must be 0, 1, 2 or 3")
endif()

option(PSPRECOMP_LTO "Enable link-time/whole-program optimization" OFF)
option(PSPRECOMP_NATIVE_AVX2 "Use AVX2 code generation for optimized MSVC builds" OFF)
option(PSPRECOMP_HOST_AVX "Use AVX for host-side scalar floor/ceil hot paths on MSVC" ON)
option(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH
    "Remove per-store write-watch branch from production AOT fast memory" OFF)
option(PSPRECOMP_AOT_PRODUCTION_FASTPATHS
    "Compile out AOT chain diagnostic/counter branches for production builds" OFF)
set(PSPRECOMP_MSVC_CGTHREADS "0" CACHE STRING
    "MSVC LTCG code-generation threads (0 = compiler default)")
if(NOT PSPRECOMP_MSVC_CGTHREADS MATCHES "^[0-9]+$")
    message(FATAL_ERROR "PSPRECOMP_MSVC_CGTHREADS must be a non-negative integer")
endif()

# Bare /MP lets cl.exe spawn one front-end per logical core, and MSBuild /m
# multiplies that by the number of projects it builds in parallel.  On the VCS
# profile each generated AOT unit costs well over a gigabyte at /Ox /Ob3, so the
# unbounded product exhausts physical memory.  Cap the per-cl.exe degree here and
# let the build scripts pick a memory-aware value.
set(PSPRECOMP_MSVC_MP_JOBS "0" CACHE STRING
    "Max cl.exe front-ends (0 = logical-core default, 1 = disable /MP)")
if(NOT PSPRECOMP_MSVC_MP_JOBS MATCHES "^[0-9]+$")
    message(FATAL_ERROR "PSPRECOMP_MSVC_MP_JOBS must be a non-negative integer")
endif()
if(PSPRECOMP_MSVC_MP_JOBS STREQUAL "0")
    set(PSPRECOMP_MSVC_MP_FLAG "/MP")
elseif(PSPRECOMP_MSVC_MP_JOBS STREQUAL "1")
    # Omitting /MP is materially different from /MP1: the latter still creates
    # and communicates with a child compiler process and can fail with D8040.
    set(PSPRECOMP_MSVC_MP_FLAG "")
else()
    set(PSPRECOMP_MSVC_MP_FLAG "/MP${PSPRECOMP_MSVC_MP_JOBS}")
endif()

if(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH)
    add_compile_definitions(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH=1)
endif()
if(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
    add_compile_definitions(PSPRECOMP_AOT_PRODUCTION_FASTPATHS=1)
endif()
if(MSVC AND PSPRECOMP_NATIVE_AVX2)
    add_compile_options($<$<CONFIG:Release>:/arch:AVX2>
                        $<$<CONFIG:RelWithDebInfo>:/arch:AVX2>)
endif()

if(PSPRECOMP_LTO)
    include(CheckIPOSupported)
    check_ipo_supported(RESULT PSPRECOMP_LTO_SUPPORTED OUTPUT PSPRECOMP_LTO_REASON)
    if(PSPRECOMP_LTO_SUPPORTED)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL ON)
    else()
        message(WARNING "Whole-program optimization unavailable: ${PSPRECOMP_LTO_REASON}")
    endif()
endif()

# Keep the runtime as an object library so profile executables own its objects.
# On Windows this also lets WINDOWS_EXPORT_ALL_SYMBOLS include runtime symbols
# in the executable's import library for separately linked overlay modules.
add_library(psprecomp_core OBJECT
    src/decoder.cpp
    src/deflate.cpp
    src/elf32.cpp
    src/guest_memory.cpp
    src/interpreter.cpp
    src/nid_registry.cpp
    src/program_analysis.cpp
    src/runtime.cpp
    src/sha256.cpp
)
target_include_directories(psprecomp_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(psprecomp_core PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:_CRT_SECURE_NO_WARNINGS>)
if(MSVC)
    target_compile_options(psprecomp_core PRIVATE /W4 /permissive- /EHsc ${PSPRECOMP_MSVC_MP_FLAG}
        $<$<CONFIG:Release>:/Ob3> $<$<CONFIG:RelWithDebInfo>:/Ob3>)
else()
    target_compile_options(psprecomp_core PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wshadow)
endif()

add_executable(psp_analyze tools/analyzer_main.cpp)
target_link_libraries(psp_analyze PRIVATE psprecomp_core)
target_compile_definitions(psp_analyze PRIVATE
    PSPRECOMP_DEFAULT_NIDS_CSV="${CMAKE_CURRENT_SOURCE_DIR}/configs/nids.csv")

add_executable(psp_recomp tools/codegen_main.cpp)
target_link_libraries(psp_recomp PRIVATE psprecomp_core)

add_executable(dump_function tools/dump_function.cpp)
target_link_libraries(dump_function PRIVATE psprecomp_core)

function(psprecomp_enable_host_avx target)
    # /arch:AVX appears after the directory-wide /arch:AVX2 option in MSVC's
    # command line and therefore silently downgrades the whole target. AVX2
    # already includes the AVX scalar instructions this compatibility switch
    # was introduced for, so do not emit the weaker override in an AVX2 build.
    if(PSPRECOMP_HOST_AVX AND MSVC AND NOT PSPRECOMP_NATIVE_AVX2)
        target_compile_options(${target} PRIVATE /arch:AVX)
    endif()
endfunction()

if(PSPRECOMP_BUILD_TESTS)
    enable_testing()
    add_executable(psprecomp_tests tests/test_main.cpp)
    target_link_libraries(psprecomp_tests PRIVATE psprecomp_core)
    add_dependencies(psprecomp_tests psp_recomp)
    target_compile_definitions(psprecomp_tests PRIVATE
        PSPRECOMP_CODEGEN_PATH="$<TARGET_FILE:psp_recomp>")
    add_test(NAME psprecomp_tests COMMAND psprecomp_tests)
endif()

if(NOT PSPRECOMP_PROFILE STREQUAL "")
    set(PSPRECOMP_PROFILE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/profiles/${PSPRECOMP_PROFILE}")
    if(NOT EXISTS "${PSPRECOMP_PROFILE_DIR}/CMakeLists.txt")
        message(FATAL_ERROR "Unknown PSPRecomp profile: ${PSPRECOMP_PROFILE}")
    endif()
    add_subdirectory("${PSPRECOMP_PROFILE_DIR}" "profiles/${PSPRECOMP_PROFILE}")
endif()

# Build the executables registered with CTest without building the game or
# overlays. Test names match their executable targets in the framework/profile.
if(PSPRECOMP_BUILD_TESTS)
    get_property(_psprecomp_test_targets DIRECTORY PROPERTY TESTS)
    if(NOT PSPRECOMP_PROFILE STREQUAL "")
        get_property(_psprecomp_profile_tests DIRECTORY "${PSPRECOMP_PROFILE_DIR}" PROPERTY TESTS)
        list(APPEND _psprecomp_test_targets ${_psprecomp_profile_tests})
    endif()
    add_custom_target(psprecomp_test_binaries DEPENDS ${_psprecomp_test_targets})
    if(PSPRECOMP_COVERAGE)
        add_custom_target(psprecomp_coverage_binaries
            DEPENDS psprecomp_test_binaries psp_analyze psp_recomp dump_function)
        if(TARGET Yakumo)
            add_dependencies(psprecomp_coverage_binaries Yakumo)
        endif()
    endif()
endif()

if(PSPRECOMP_BUILD_TESTS)
    add_executable(psprecomp_runtime_contract_tests tests/runtime_contract_tests.cpp)
    target_link_libraries(psprecomp_runtime_contract_tests PRIVATE psprecomp_core)
    add_dependencies(psprecomp_runtime_contract_tests psp_analyze psp_recomp dump_function)
    target_compile_definitions(psprecomp_runtime_contract_tests PRIVATE
        PSPRECOMP_ANALYZE_PATH="$<TARGET_FILE:psp_analyze>"
        PSPRECOMP_RECOMP_PATH="$<TARGET_FILE:psp_recomp>"
        PSPRECOMP_DUMP_PATH="$<TARGET_FILE:dump_function>"
        PSPRECOMP_TEST_COMPILER_PATH="${CMAKE_CXX_COMPILER}"
        PSPRECOMP_TEST_INCLUDE_PATH="${PROJECT_SOURCE_DIR}/include"
        PSPRECOMP_TEST_CMAKE_PATH="${CMAKE_COMMAND}"
        PSPRECOMP_TEST_GENERATOR="${CMAKE_GENERATOR}")
    add_test(NAME psprecomp_runtime_contract_tests COMMAND psprecomp_runtime_contract_tests)
    set_tests_properties(psprecomp_runtime_contract_tests PROPERTIES TIMEOUT 60)
    add_test(NAME psprecomp_memory_watch_contracts COMMAND psprecomp_runtime_contract_tests --memory-watch)
    set_tests_properties(psprecomp_memory_watch_contracts PROPERTIES TIMEOUT 60
        ENVIRONMENT "PSPRECOMP_WATCH_WRITE=0x08804000;PSPRECOMP_WATCH_WRITE_SIZE=4;PSPRECOMP_TRACE_VRAM_READS=1")
    add_dependencies(psprecomp_test_binaries psprecomp_runtime_contract_tests)
endif()
