cmake_minimum_required(VERSION 3.20)

# Host application for the N64Recomp output of Conker's Bad Fur Day (US).
# RecompiledFuncs/ must exist first (`python recomp/recompile.py`; build.cmd / build.sh run it).
#
# Windows (RT64 window, keyboard and controllers) with MSVC:
#   host\build_windows.cmd   (sets up MSVC, then configures and builds host/build-win)
# Linux/WSL and macOS (Metal; use Apple's clang, /usr/bin/clang):
#   cmake -S host -B host/build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=RelWithDebInfo
#   cmake --build host/build

project(ConkerRecomp C CXX)
if (APPLE)
    # RT64 and RecompFrontend have Objective-C++ sources (Metal, Cocoa); rt64 enables
    # the languages only in its own directory.
    enable_language(OBJC OBJCXX)
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_EXTENSIONS OFF)
# MSVC compiles C as C89 unless told otherwise; o1heap needs C99.
set(CMAKE_C_STANDARD 11)

# OFF builds a headless host (null renderer, no input or sound), for tests.
option(CONKER_RT64 "Render with RT64 in an SDL window" ON)

set(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
set(RT64_DIR "${REPO_ROOT}/tools/rt64")

add_subdirectory("${REPO_ROOT}/tools/N64ModernRuntime" "${CMAKE_CURRENT_BINARY_DIR}/N64ModernRuntime")
if (MSVC)
    # MSVC 14.51 hits an internal compiler error optimising librecomp's flash.cpp.
    set_source_files_properties("${REPO_ROOT}/tools/N64ModernRuntime/librecomp/src/flash.cpp"
        DIRECTORY "${REPO_ROOT}/tools/N64ModernRuntime/librecomp"
        PROPERTIES COMPILE_OPTIONS "/Od")
endif()

# The recompiled game code. recomp/recompile.py records hashes of the files that shape it; stop if
# any has changed since, as the build would otherwise quietly use stale code.
set(RECOMP_INPUTS conker.toml recomp/conker.us.syms.toml recomp/code_rewrites.txt recomp/n64recomp.patch recomp/unpack_rom.py recomp/emit_tlb_pages.py recomp/audio_ucode.toml)
set(RECOMP_STAMP "${REPO_ROOT}/RecompiledFuncs/inputs.sha256")
if (NOT EXISTS "${RECOMP_STAMP}")
    message(FATAL_ERROR "RecompiledFuncs/ is missing or predates input tracking: run build.cmd / build.sh, or `python recomp/recompile.py`.")
endif()
file(READ "${RECOMP_STAMP}" RECOMP_STAMP_TEXT)
set(RECOMP_EXPECTED "")
foreach(input ${RECOMP_INPUTS})
    file(READ "${REPO_ROOT}/${input}" input_text)
    string(REPLACE "\r" "" input_text "${input_text}")
    string(SHA256 input_hash "${input_text}")
    string(APPEND RECOMP_EXPECTED "${input_hash} ${input}\n")
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${REPO_ROOT}/${input}")
endforeach()
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${RECOMP_STAMP}")
if (NOT RECOMP_STAMP_TEXT STREQUAL RECOMP_EXPECTED)
    message(FATAL_ERROR "RecompiledFuncs/ is out of date (${RECOMP_INPUTS} changed since it was generated): run build.cmd / build.sh, or `python recomp/recompile.py`, then build again.")
endif()
file(GLOB RECOMPILED_FUNCS CONFIGURE_DEPENDS "${REPO_ROOT}/RecompiledFuncs/funcs_*.c")
add_library(RecompiledFuncs STATIC ${RECOMPILED_FUNCS} "${REPO_ROOT}/RecompiledFuncs/tlb_pages.c")
target_include_directories(RecompiledFuncs PRIVATE
    "${REPO_ROOT}/tools/N64ModernRuntime/N64Recomp/include"
    "${REPO_ROOT}/RecompiledFuncs"
)
# funcs.h does not declare the libultra functions N64Recomp skips (the runtime
# provides them), and the generated code has many unused labels and variables.
# setjmp.h is for conker.toml's hooks on the script interpreter's abort (func_150AE280).
if (MSVC)
    target_compile_options(RecompiledFuncs PRIVATE /W0 /bigobj /FIsetjmp.h)
else()
    target_compile_options(RecompiledFuncs PRIVATE
        -include setjmp.h
        -Wno-implicit-function-declaration
        -Wno-unused-label
        -Wno-unused-variable
        -Wno-unused-but-set-variable
        -fno-strict-aliasing
    )
endif()

if (ANDROID)
    # A library for the app in android/ to load (libConkerRecomp.so). With RT64, SDL's
    # SDLActivity calls main.cpp's main (SDL_main); the headless build is started through
    # src/android_main.cpp, which also sends the output to logcat.
    add_library(ConkerRecomp SHARED src/android_main.cpp)
    target_link_libraries(ConkerRecomp PRIVATE log android)

    # Custom Vulkan drivers (Turnip and the like) through libadrenotools, 64-bit only: as in
    # codepdbh/nfsmw-android. Its hooks are loaded from the app's native library folder.
    if (ANDROID_ABI STREQUAL "arm64-v8a")
        add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../tools/libadrenotools" libadrenotools)
        add_library(conker_gpu_driver SHARED src/android/gpu_driver.cpp)
        target_link_libraries(conker_gpu_driver PRIVATE adrenotools android log dl)
        set_target_properties(conker_gpu_driver PROPERTIES CXX_VISIBILITY_PRESET hidden)
        # linkernsbypass has data symbols named like libdl's functions: exporting them would
        # interpose them when Android's loader opens its vendor namespace (SIGSEGV).
        target_link_options(conker_gpu_driver PRIVATE "-Wl,--exclude-libs,ALL")
        target_link_options(hook_impl PRIVATE "-Wl,--exclude-libs,ALL")
        foreach(driver_target conker_gpu_driver main_hook file_redirect_hook gsl_alloc_hook hook_impl)
            target_link_options(${driver_target} PRIVATE "-Wl,-z,max-page-size=16384")
        endforeach()
        target_link_libraries(ConkerRecomp PRIVATE conker_gpu_driver)
        add_dependencies(ConkerRecomp main_hook file_redirect_hook gsl_alloc_hook hook_impl)
        target_compile_definitions(ConkerRecomp PRIVATE CONKER_GPU_DRIVERS)
    endif()
else()
    add_executable(ConkerRecomp)
endif()
target_sources(ConkerRecomp PRIVATE
    src/main.cpp
    src/null_renderer.cpp
    src/overlays.cpp
    src/ultra_extras.cpp
    src/mod_api.cpp
    src/rom_versions.cpp
    src/widescreen.cpp
    src/interpolation.cpp
    src/render_fixes.cpp
    src/look_aim.cpp
    src/mouse_camera.cpp
    # The audio microcode, recompiled by RSPRecomp (recomp/audio_ucode.toml, run by recomp/recompile.py).
    "${REPO_ROOT}/RecompiledFuncs/rsp/audio_ucode.cpp"
)
if (MSVC)
    set_source_files_properties("${REPO_ROOT}/RecompiledFuncs/rsp/audio_ucode.cpp" PROPERTIES COMPILE_OPTIONS "/W0;/bigobj")
endif()
target_include_directories(ConkerRecomp PRIVATE
    "${REPO_ROOT}/tools/N64ModernRuntime/N64Recomp/include"
    "${REPO_ROOT}/RecompiledFuncs"
)
target_link_libraries(ConkerRecomp PRIVATE RecompiledFuncs librecomp ultramodern)

if (CONKER_RT64)
    set(RT64_STATIC ON CACHE BOOL "" FORCE)
    # RT64 only needs zstd's library. Its programs also copy man pages with a shell
    # command that breaks on a path with an apostrophe.
    set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
    if (ANDROID)
        # SDL2 from source (tools/SDL2), as the shared library SDLActivity loads.
        set(SDL_SHARED ON CACHE BOOL "" FORCE)
        set(SDL_STATIC OFF CACHE BOOL "" FORCE)
        set(SDL_TEST OFF CACHE BOOL "" FORCE)
        add_subdirectory("${REPO_ROOT}/tools/SDL2" "${CMAKE_CURRENT_BINARY_DIR}/SDL2")
        # Both <SDL.h> and <SDL2/SDL.h>: SDL's build copies its headers into include/SDL2.
        set(SDL2_INCLUDE_DIRS "${REPO_ROOT}/tools/SDL2/include" "${CMAKE_CURRENT_BINARY_DIR}/SDL2/include")
        # zstd's dictionary builder needs qsort_r, which Android lacks; RT64 doesn't use it.
        set(ZSTD_BUILD_DICTBUILDER OFF CACHE BOOL "" FORCE)
        set(SDL2_LIBRARIES SDL2::SDL2)
        # FreeType from source (tools/freetype) for RmlUi, found by its find_package(Freetype).
        foreach(option FT_DISABLE_ZLIB FT_DISABLE_BZIP2 FT_DISABLE_PNG FT_DISABLE_HARFBUZZ FT_DISABLE_BROTLI)
            set(${option} ON CACHE BOOL "" FORCE)
        endforeach()
        add_subdirectory("${REPO_ROOT}/tools/freetype" "${CMAKE_CURRENT_BINARY_DIR}/freetype" EXCLUDE_FROM_ALL)
        add_library(Freetype::Freetype ALIAS freetype)
        set(FREETYPE_LIBRARY freetype CACHE STRING "" FORCE)
        set(FREETYPE_INCLUDE_DIR_ft2build "${REPO_ROOT}/tools/freetype/include" CACHE PATH "" FORCE)
        set(FREETYPE_INCLUDE_DIR_freetype2 "${REPO_ROOT}/tools/freetype/include" CACHE PATH "" FORCE)
        # Android has no native file dialog: RT64 and RecompFrontend get this nfd instead.
        add_library(nfd STATIC src/android/nfd_android.cpp)
        target_include_directories(nfd PUBLIC "${RT64_DIR}/src/contrib/nativefiledialog-extended/src/include")
        # RT64 renders with Vulkan into the SDL window's ANativeWindow (plume's Android path).
    elseif (NOT WIN32 AND NOT APPLE)
        # On Linux, RT64 renders with Vulkan into an SDL window (as in
        # Banjo: Recompiled): the window is created with SDL_WINDOW_VULKAN.
        # On macOS it renders with Metal into the window's CAMetalLayer.
        set(RT64_SDL_WINDOW_VULKAN ON CACHE BOOL "" FORCE)
        add_compile_definitions(PLUME_SDL_VULKAN_ENABLED RT64_SDL_WINDOW_VULKAN)
        # The file dialogs (Load ROM, mods) ask the desktop for its own through
        # xdg-desktop-portal (D-Bus) instead of opening a GTK one in the game: GTK
        # crashed building its dialog, which the launcher opens from its own thread,
        # not the one GTK started on.
        set(NFD_PORTAL ON CACHE BOOL "" FORCE)
    endif()
    add_subdirectory("${RT64_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/rt64")

    # RecompFrontend: the launcher, config and mod menus (recompui, on RmlUi) and
    # remappable input (recompinput). tools/RecompFrontend is an untracked checkout.
    if (WIN32)
        set(sdl2_SOURCE_DIR "${RT64_DIR}/src/contrib/mupen64plus-win32-deps/SDL2-2.26.3")
    elseif (NOT ANDROID)
        # The system SDL2; RecompFrontend takes its headers from SDL2_INCLUDE_DIRS.
        find_package(SDL2 REQUIRED)
    endif()
    set(RECOMP_FRONTEND_N64MODERNRUNTIME_PATH "${REPO_ROOT}/tools/N64ModernRuntime")
    set(RECOMP_FRONTEND_RT64_PATH "${RT64_DIR}")
    # recompui compiles its shaders with rt64's build_*_shader functions, which use
    # these variables from rt64's own directory scope: repeat them here.
    set(DXC_COMMON_OPTS "-I${RT64_DIR}/src")
    set(DXC_DXIL_OPTS "-Wno-ignored-attributes")
    set(DXC_SPV_OPTS "-spirv" "-fspv-target-env=vulkan1.0" "-fvk-use-dx-layout")
    set(DXC_PS_OPTS "${DXC_COMMON_OPTS}" "-E" "PSMain" "-T ps_6_0" "-D DYNAMIC_RENDER_PARAMS")
    set(DXC_VS_OPTS "${DXC_COMMON_OPTS}" "-E" "VSMain" "-T vs_6_0" "-D DYNAMIC_RENDER_PARAMS" "-fvk-invert-y")
    if (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
        set(DXC_ARCH x64)
    else()
        set(DXC_ARCH arm64)
    endif()
    # rt64's shader functions also run file_to_c: a CMake script when cross-compiling
    # (as rt64's CMakeLists.txt sets it), the program otherwise.
    if (CMAKE_CROSSCOMPILING)
        set(FILE_TO_C "${CMAKE_COMMAND}" -P "${RT64_DIR}/src/tools/file_to_c/file_to_c.cmake")
        set(FILE_TO_C_DEPENDS "${RT64_DIR}/src/tools/file_to_c/file_to_c.cmake")
    else()
        set(FILE_TO_C file_to_c)
        set(FILE_TO_C_DEPENDS file_to_c)
    endif()
    if (CMAKE_CROSSCOMPILING AND CMAKE_HOST_WIN32)
        # Android from Windows: DXC runs on the build machine.
        set(DXC "${RT64_DIR}/src/contrib/dxc/bin/x64/dxc.exe")
    elseif (WIN32)
        set(DXC "${RT64_DIR}/src/contrib/dxc/bin/x64/dxc.exe")
    elseif (APPLE)
        # recompui's shaders also go through spirv_cross_msl (rt64's target) and xcrun metal.
        set(DXC "DYLD_LIBRARY_PATH=${RT64_DIR}/src/contrib/dxc/lib/${DXC_ARCH}" "${RT64_DIR}/src/contrib/dxc/bin/${DXC_ARCH}/dxc-macos")
    else()
        set(DXC "LD_LIBRARY_PATH=${RT64_DIR}/src/contrib/dxc/lib/${DXC_ARCH}" "${RT64_DIR}/src/contrib/dxc/bin/${DXC_ARCH}/dxc-linux")
    endif()
    add_subdirectory("${REPO_ROOT}/tools/RecompFrontend" "${CMAKE_CURRENT_BINARY_DIR}/RecompFrontend")
    if (MSVC)
        # Their GCC-style -Wno-unused-parameter is an error for cl.
        set_property(TARGET recompui recompinput PROPERTY COMPILE_OPTIONS "")
    endif()
    target_link_libraries(ConkerRecomp PRIVATE recompui recompinput nfd)
    # recompui loads its fonts, icons and style sheet from assets/ next to the executable.
    add_custom_command(TARGET ConkerRecomp POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/assets" "$<TARGET_FILE_DIR:ConkerRecomp>/assets")

    target_sources(ConkerRecomp PRIVATE src/frontend.cpp src/conker_config.cpp src/audio_output.cpp)
    if (ANDROID)
        target_sources(ConkerRecomp PRIVATE src/android/touch_controls.cpp)
    endif()
    # CONKER_RT64 selects the window build; the rest are what rt64 compiles its own headers with.
    target_compile_definitions(ConkerRecomp PRIVATE CONKER_RT64 HLSL_CPU)
    if (WIN32)
        target_compile_definitions(ConkerRecomp PRIVATE NOMINMAX)
    endif()
    # rt64 sets its include paths per directory, so repeat the ones its headers need.
    target_include_directories(ConkerRecomp PRIVATE
        "${RT64_DIR}/src"
        "${RT64_DIR}/src/contrib"
        "${RT64_DIR}/src/contrib/plume"
        "${RT64_DIR}/src/contrib/imgui"
        "${RT64_DIR}/src/contrib/hlslpp/include"
        "${RT64_DIR}/src/contrib/mupen64plus-core/src/api"
        "${RT64_DIR}/src/contrib/nativefiledialog-extended/src/include"
    )
    target_link_libraries(ConkerRecomp PRIVATE rt64)

    if (WIN32)
        set(SDL2_DIR "${RT64_DIR}/src/contrib/mupen64plus-win32-deps/SDL2-2.26.3")
        target_include_directories(ConkerRecomp PRIVATE "${SDL2_DIR}/include" "${RT64_DIR}/src/contrib/dxc/inc")
        target_link_directories(ConkerRecomp PRIVATE "${SDL2_DIR}/lib/x64")
        target_link_libraries(ConkerRecomp PRIVATE SDL2)
        # Runtime DLLs next to the executable: SDL2, and DXC which RT64 loads at startup.
        add_custom_command(TARGET ConkerRecomp POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${SDL2_DIR}/lib/x64/SDL2.dll"
                "${RT64_DIR}/src/contrib/dxc/bin/x64/dxcompiler.dll"
                "${RT64_DIR}/src/contrib/dxc/bin/x64/dxil.dll"
                "$<TARGET_FILE_DIR:ConkerRecomp>")
    else()
        target_include_directories(ConkerRecomp PRIVATE ${SDL2_INCLUDE_DIRS})
        target_link_libraries(ConkerRecomp PRIVATE ${SDL2_LIBRARIES})
    endif()
endif()

if (NOT MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
    # The recompiled audio microcode's vector unit uses SSSE3 and SSE4.1 intrinsics,
    # which GCC and Clang only allow when targeting them (MSVC always does).
    target_compile_options(ConkerRecomp PRIVATE -msse4.1)
endif()

if (UNIX)
    # Exported symbols let the crash handler's backtrace name recompiled functions.
    target_link_options(ConkerRecomp PRIVATE -rdynamic)
elseif (WIN32)
    # The crash handler symbolises the stack with DbgHelp.
    target_link_libraries(ConkerRecomp PRIVATE DbgHelp)
    # Mods replace recompiled functions by patching their machine code, so identical
    # functions must not be folded into one.
    target_link_options(ConkerRecomp PRIVATE /OPT:NOICF)
endif()
