cmake_minimum_required(VERSION 3.20)
project(BO3Recomp LANGUAGES C CXX)

# Black Ops III (Xbox 360) static recompilation - Linux native build.
#
# Layout:
#   ppc/mp, ppc/zm      C++ generated by XenonRecomp from default.xex / default_zm.xex (+TU1)
#                       (regenerate with tools/prepare.sh; never edit by hand)
#   BO3RecompLib_MP/ZM  static libraries with the recompiled guest code
#
# MP and ZM are separate executables on the console and define the same sub_XXXXXXXX
# symbols, so they are built as separate libraries and must be linked into separate binaries.

if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    message(FATAL_ERROR "XenonRecomp output requires Clang (uses clang-specific attributes). "
                        "Configure with -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++")
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# CPU baseline. x86-64-v3 (AVX2, BMI2, MOVBE, FMA, LZCNT; Haswell / Zen and newer): every guest
# load/store is a byte-swapped access, which MOVBE does in one instruction. FP contraction stays
# off so float results match the PowerPC instructions. -DBO3_MARCH=sandybridge for older CPUs.
if(ANDROID)
    # Android ARM64 (Cortex-A76/A55 class: Snapdragon 7xx/8xx)
    set(BO3_MARCH "armv8.2-a" CACHE STRING "-march value for the recompiled code and the runtime")
else()
    set(BO3_MARCH "x86-64-v3" CACHE STRING "-march value for the recompiled code and the runtime")
endif()

set(XENONRECOMP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/tools/XenonRecomp)
set(SIMDE_INCLUDE ${XENONRECOMP_ROOT}/thirdparty/simde)

function(bo3_add_recomp_lib target ppc_dir)
    file(GLOB RECOMP_SOURCES CONFIGURE_DEPENDS ${ppc_dir}/ppc_recomp.*.cpp ${ppc_dir}/ppc_func_mapping.cpp)
    if(NOT RECOMP_SOURCES)
        message(WARNING "${ppc_dir} is empty - run tools/prepare.sh first. Skipping ${target}.")
        return()
    endif()
    add_library(${target} STATIC ${RECOMP_SOURCES})
    target_include_directories(${target} PUBLIC ${ppc_dir} ${SIMDE_INCLUDE})
    target_compile_options(${target} PRIVATE
        -march=${BO3_MARCH}
        -ffp-contract=off
        -fno-strict-aliasing
        -Wno-unused-label
        -Wno-unused-variable
        -Wno-unused-but-set-variable
        -Wno-switch                 # generated switches over guest values
    )
endfunction()

bo3_add_recomp_lib(BO3RecompLib_MP ${CMAKE_CURRENT_SOURCE_DIR}/ppc/mp)
bo3_add_recomp_lib(BO3RecompLib_ZM ${CMAKE_CURRENT_SOURCE_DIR}/ppc/zm)

# ---------------------------------------------------------------------------------------
# Third-party: SDL3 (window, input) and glslang (runtime GLSL -> SPIR-V), built static.
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
set(SDL_TESTS OFF CACHE BOOL "" FORCE)
set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(thirdparty/SDL3 EXCLUDE_FROM_ALL)

set(ENABLE_OPT OFF CACHE BOOL "" FORCE)
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "" FORCE)
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "" FORCE)
set(ENABLE_HLSL OFF CACHE BOOL "" FORCE)
set(GLSLANG_TESTS OFF CACHE BOOL "" FORCE)
set(GLSLANG_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_subdirectory(thirdparty/glslang EXCLUDE_FROM_ALL)

find_package(Vulkan REQUIRED)

# ---------------------------------------------------------------------------------------
# Runtime (host side of the port). Compiled once per executable because it uses that
# executable's ppc_config.h (image/code layout for the function table).
file(GLOB RUNTIME_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/runtime/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/runtime/src/gpu/*.cpp)
if(ANDROID)
    # XenonUtils (xex loader) from source: the prebuilt libraries are the desktop's.
    set(XU ${XENONRECOMP_ROOT}/XenonUtils)
    set(XT ${XENONRECOMP_ROOT}/thirdparty)
    add_library(XenonUtilsAndroid STATIC ${XU}/disasm.cpp ${XU}/xex.cpp ${XU}/image.cpp ${XU}/xdbf_wrapper.cpp ${XU}/xex_patcher.cpp
        ${XU}/memory_mapped_file.cpp ${XT}/libmspack/libmspack/mspack/lzxd.c ${XT}/tiny-AES-c/aes.c ${XT}/disasm/disasm.c ${XT}/disasm/ppc-dis.c)
    target_compile_definitions(XenonUtilsAndroid PRIVATE NOMINMAX)
    target_include_directories(XenonUtilsAndroid PUBLIC ${XU} ${SIMDE_INCLUDE} ${XT}/disasm
        PRIVATE ${XT}/libmspack/libmspack/mspack ${XT}/tiny-AES-c ${XT}/TinySHA1)
    set(XENONUTILS_LIBS XenonUtilsAndroid)
    # libadrenotools (BSD-2): loads Turnip (tools/build_turnip_android.sh) instead of the stock driver.
    # Its hook libraries go into the APK next to the game library.
    add_subdirectory(thirdparty/libadrenotools EXCLUDE_FROM_ALL)
    # FFmpeg 7.1 (xma2 decoder only) cross-built with the NDK: tools/build_ffmpeg_android.sh
    set(FFMPEG_ANDROID ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg-android)
    set(AVCODEC_LIB ${FFMPEG_ANDROID}/lib/libavcodec.so)
    set(AVUTIL_LIB ${FFMPEG_ANDROID}/lib/libavutil.so)
    if(NOT EXISTS ${AVCODEC_LIB})
        message(FATAL_ERROR "FFmpeg for Android not built: run tools/build_ffmpeg_android.sh")
    endif()
else()
    set(XENONUTILS_LIBS
        ${XENONRECOMP_ROOT}/build/XenonUtils/libXenonUtils.a
        ${XENONRECOMP_ROOT}/build/thirdparty/disasm/libdisasm.a
        ${XENONRECOMP_ROOT}/build/thirdparty/fmt/libfmt.a)

    # FFmpeg (XMA audio): static, xma2 only (tools/build_ffmpeg_desktop.sh - release builds, no
    # dependency on the system's FFmpeg); otherwise the system's libavcodec/libavutil 7.1 + headers
    # of the same version in thirdparty/ffmpeg-src (no -devel package needed).
    set(FFMPEG_DESKTOP ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg-desktop)
    if(EXISTS ${FFMPEG_DESKTOP}/lib/libavcodec.a)
        set(AVCODEC_LIB ${FFMPEG_DESKTOP}/lib/libavcodec.a)
        set(AVUTIL_LIB ${FFMPEG_DESKTOP}/lib/libavutil.a m pthread)
        message(STATUS "FFmpeg: static xma2-only build in thirdparty/ffmpeg-desktop")
    endif()
    find_library(AVCODEC_LIB NAMES avcodec libavcodec.so.61 PATHS /usr/lib64 /usr/lib)
    find_library(AVUTIL_LIB NAMES avutil libavutil.so.59 PATHS /usr/lib64 /usr/lib)
    if(NOT AVCODEC_LIB OR NOT AVUTIL_LIB)
        message(FATAL_ERROR "libavcodec/libavutil (FFmpeg 7.1) not found")
    endif()
endif()

function(bo3_add_game target lib kind)
    if(NOT TARGET ${lib})
        return()
    endif()
    file(GLOB GAME_HOOKS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/runtime/src/${kind}/*.cpp)
    if(ANDROID)
        # loaded by SDL's Java activity (lib<target>.so, entry SDL_main)
        add_library(${target} SHARED ${RUNTIME_SOURCES} ${GAME_HOOKS})
    else()
        add_executable(${target} ${RUNTIME_SOURCES} ${GAME_HOOKS})
    endif()
    target_compile_definitions(${target} PRIVATE BO3_EXE_KIND="${kind}" VK_NO_PROTOTYPES)
    target_sources(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/volk/volk.c)
    target_include_directories(${target} PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/runtime/src
        ${XENONRECOMP_ROOT}/XenonUtils
        ${XENONRECOMP_ROOT}/thirdparty/disasm
        ${CMAKE_CURRENT_SOURCE_DIR}/runtime/xenia
        ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/glslang
        ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/volk
        $<$<BOOL:${ANDROID}>:${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/Vulkan-Headers/include>
        ${Vulkan_INCLUDE_DIRS}
        ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg-src)
    # online backend defaults of release builds (tools/make_release.sh); empty = none
    set(BO3_DEFAULT_SUPABASE_URL "" CACHE STRING "Supabase project URL compiled in as the default")
    set(BO3_DEFAULT_SUPABASE_KEY "" CACHE STRING "Supabase publishable key compiled in as the default")
    target_compile_definitions(${target} PRIVATE BO3_DEFAULT_SUPABASE_URL="${BO3_DEFAULT_SUPABASE_URL}" BO3_DEFAULT_SUPABASE_KEY="${BO3_DEFAULT_SUPABASE_KEY}")
    target_compile_options(${target} PRIVATE -march=${BO3_MARCH} -ffp-contract=off -g -Wall -Wno-unused-function -Wno-missing-braces)
    target_link_libraries(${target} PRIVATE ${lib} ${XENONUTILS_LIBS} SDL3::SDL3-static glslang::glslang
        glslang::glslang-default-resource-limits glslang::SPIRV ${AVCODEC_LIB} ${AVUTIL_LIB} ${CMAKE_DL_LIBS})
    if(ANDROID)
        target_compile_definitions(${target} PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
        target_link_libraries(${target} PRIVATE log android adrenotools)
        add_dependencies(${target} main_hook file_redirect_hook gsl_alloc_hook hook_impl)
    else()
        target_link_libraries(${target} PRIVATE pthread)
        # portable release binaries: no dependency on the distribution's C++ runtime version
        target_link_options(${target} PRIVATE -static-libstdc++ -static-libgcc)
    endif()
endfunction()

bo3_add_game(bo3_mp BO3RecompLib_MP mp)
bo3_add_game(bo3_zm BO3RecompLib_ZM zm)

# Offline shader translator test (no game code).
if(NOT ANDROID)
add_executable(shader_test tools/shader_test/shader_test.cpp runtime/src/gpu/shader_translator.cpp runtime/src/gpu/shader_compiler.cpp)
target_include_directories(shader_test PRIVATE runtime/src runtime/xenia thirdparty/glslang)
target_link_libraries(shader_test PRIVATE glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV)
endif()
