cmake_minimum_required(VERSION 3.20)
# macOS: the `wwhd` executable (Metal). Android: `libwwhd.so` (Vulkan), loaded by the app in android/.
if(APPLE)
  project(wwhd_recomp C CXX OBJCXX)
else()
  project(wwhd_recomp C CXX ASM)
endif()

# no paths of the build machine in the binaries (__FILE__ in checks, debug info, the ops.c bitcode):
# file names are relative to the repository
add_compile_options("-ffile-prefix-map=${CMAKE_SOURCE_DIR}/=" "-ffile-prefix-map=${CMAKE_BINARY_DIR}/=build/")
# Linux and Android on arm64: plain char is unsigned in that ABI, but signed on x86-64 and Apple
# arm64, where the runtime was written and tested (original project 2a54901)
if(CMAKE_SYSTEM_NAME MATCHES "^(Linux|Android)$" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
  add_compile_options(-fsigned-char)
endif()

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

# The game code: either recompiled on the PC (tools/recomp/recomp.py -> build/gen, linked in), or,
# for an APK without game code, recompiled on the device and loaded at startup (runtime/src/recomp)
option(WWHD_DEVICE_RECOMP "Android: no build/gen; the app recompiles the game from the user's disc image" OFF)
option(WWHD_PUBLIC_RELEASE "A published release: developer exports (shader template) off" OFF)
if(NOT WWHD_DEVICE_RECOMP)
  # Generated by tools/recomp/recomp.py from game/code/cking.rpx
  set(GEN_DIR ${CMAKE_SOURCE_DIR}/build/gen CACHE PATH "recompiler output")
  file(GLOB GEN_SOURCES CONFIGURE_DEPENDS ${GEN_DIR}/code_*.c)
  if(NOT GEN_SOURCES)
    message(FATAL_ERROR "no generated code in ${GEN_DIR}; run tools/recomp/recomp.py first")
  endif()

  # testing the LLVM recompiler (runtime/src/recomp, runtime/tools/recomp_llvm.cpp): its object files
  # replace the C functions; the function table and import stubs still come from GEN_DIR
  set(GEN_OBJECTS "" CACHE PATH "directory of code_*.o from recomp_llvm (instead of GEN_DIR/code_*.c)")
  if(GEN_OBJECTS)
    file(GLOB GEN_SOURCES CONFIGURE_DEPENDS ${GEN_OBJECTS}/code_*.o)
    set_source_files_properties(${GEN_SOURCES} PROPERTIES EXTERNAL_OBJECT TRUE)
  endif()
  add_library(gamecode STATIC ${GEN_SOURCES} ${GEN_DIR}/table.c ${GEN_DIR}/imports.c)
  target_include_directories(gamecode PUBLIC runtime/include ${GEN_DIR})
  # exact FP semantics: no contraction into FMA, no fast-math
  target_compile_options(gamecode PRIVATE -O3 -ffp-contract=off -fno-strict-aliasing -w)
  if(APPLE)
    target_compile_options(gamecode PRIVATE -mcpu=apple-m1)
  endif()
endif()

# ---- vendored Cemu GPU pieces (MPL-2.0): address library + shader decompiler (MSL for Metal, GLSL for Vulkan)
set(CEMU_DIR ${CMAKE_SOURCE_DIR}/runtime/third_party/cemu)
file(GLOB_RECURSE CEMU_SOURCES CONFIGURE_DEPENDS ${CEMU_DIR}/*.cpp)
if(NOT APPLE)
  list(FILTER CEMU_SOURCES EXCLUDE REGEX "LatteDecompilerEmitMSL")
endif()
add_library(cemu_latte STATIC ${CEMU_SOURCES})
target_include_directories(cemu_latte PUBLIC ${CEMU_DIR} ${CEMU_DIR}/Cafe ${CMAKE_SOURCE_DIR}/runtime/third_party/fmt/include)
target_compile_options(cemu_latte PRIVATE "SHELL:-include ${CEMU_DIR}/cemu_shim.h")
if(APPLE)
  target_include_directories(cemu_latte PUBLIC ${CMAKE_SOURCE_DIR}/runtime/third_party/metal-cpp)
  target_compile_definitions(cemu_latte PUBLIC ENABLE_METAL=1)
else()
  target_compile_definitions(cemu_latte PUBLIC ENABLE_VULKAN=1)
endif()
target_compile_options(cemu_latte PRIVATE -w -O2)

# ---- vendored zstd 1.5.7 (BSD): Cemu .wua archives (runtime/src/disc/wua.cpp) and save states
# (savestate.cpp); without its multithreaded compressor
set(ZSTD_DIR ${CMAKE_SOURCE_DIR}/runtime/third_party/zstd/lib)
file(GLOB ZSTD_SOURCES CONFIGURE_DEPENDS ${ZSTD_DIR}/common/*.c ${ZSTD_DIR}/decompress/*.c ${ZSTD_DIR}/compress/*.c)
add_library(zstd_dec STATIC ${ZSTD_SOURCES})
target_include_directories(zstd_dec PUBLIC ${ZSTD_DIR})
target_compile_definitions(zstd_dec PRIVATE ZSTD_DISABLE_ASM ZSTD_LEGACY_SUPPORT=0)
target_compile_options(zstd_dec PRIVATE -w -O2)

# ---- runtime sources shared by both platforms
# GX2 and HLE sources are compiled into the final binary (not a static library) so their strong
# definitions replace the generated weak import stubs.
file(GLOB RUNTIME_SOURCES CONFIGURE_DEPENDS runtime/src/*.cpp runtime/src/*.c runtime/src/hle/*.cpp runtime/src/mods/*.cpp
     runtime/src/disc/*.cpp)
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "audio_out_(coreaudio|aaudio)\\.cpp$")
# the macOS controls window's mapping (macOS key codes); Android maps input in runtime/src/android
if(NOT APPLE)
  list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/input_map\\.cpp$")
endif()
file(GLOB GX2_SOURCES CONFIGURE_DEPENDS runtime/src/gx2/*.cpp)
set(SHARED_GFX_SOURCES runtime/src/gfx/shader_headstart.cpp)

if(APPLE)
  # ---- macOS: the game executable, native GX2 on Metal
  file(GLOB GFX_SOURCES CONFIGURE_DEPENDS runtime/src/gfx/*.mm)
  set_source_files_properties(${GX2_SOURCES} ${GFX_SOURCES} ${SHARED_GFX_SOURCES} PROPERTIES COMPILE_OPTIONS "-include;${CEMU_DIR}/cemu_shim.h")
  set_source_files_properties(${GFX_SOURCES} PROPERTIES COMPILE_FLAGS "-fobjc-arc")
  # gameplay mods drawing with Metal (runtime/src/mods/*.mm)
  file(GLOB MOD_GFX_SOURCES CONFIGURE_DEPENDS runtime/src/mods/*.mm)
  set_source_files_properties(${MOD_GFX_SOURCES} PROPERTIES COMPILE_FLAGS "-fobjc-arc")

  add_executable(wwhd ${RUNTIME_SOURCES} runtime/src/audio_out_coreaudio.cpp ${GX2_SOURCES} ${GFX_SOURCES} ${SHARED_GFX_SOURCES}
                      ${MOD_GFX_SOURCES})
  target_include_directories(wwhd PRIVATE runtime/include runtime/src runtime/src/gx2)
  target_compile_options(wwhd PRIVATE -Wall -Wno-unused-function -Wno-unused-variable -ffp-contract=off)
  find_package(ZLIB REQUIRED)
  target_link_libraries(wwhd PRIVATE gamecode cemu_latte ZLIB::ZLIB
    "-framework Metal" "-framework QuartzCore" "-framework AppKit" "-framework Foundation" "-framework ImageIO" "-framework UniformTypeIdentifiers" "-framework GameController" "-framework Carbon" "-framework AudioToolbox" "-framework CoreAudio" compression)

  add_executable(shadertest runtime/tools/shadertest.cpp runtime/src/gx2/gx2_shader_regs.cpp)
  target_include_directories(shadertest PRIVATE runtime/include runtime/src runtime/src/gx2)
  target_link_libraries(shadertest PRIVATE cemu_latte)
  target_compile_options(shadertest PRIVATE "SHELL:-include ${CEMU_DIR}/cemu_shim.h")

  # controls mapping unit tests (runtime/src/input_map.cpp)
  add_executable(input_map_test runtime/tools/input_map_test.cpp runtime/src/input_map.cpp)
  target_include_directories(input_map_test PRIVATE runtime/src)

elseif(ANDROID)
  # ---- Android: libwwhd.so, native GX2 on Vulkan; the Java activity in android/ loads it
  include(FetchContent)
  # GLSL -> SPIR-V for the decompiled shaders
  set(ENABLE_OPT OFF CACHE BOOL "" FORCE)
  set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "" FORCE)
  set(ENABLE_HLSL OFF CACHE BOOL "" FORCE)
  set(ENABLE_SPVREMAPPER OFF CACHE BOOL "" FORCE)
  set(ENABLE_CTEST OFF CACHE BOOL "" FORCE)
  set(GLSLANG_TESTS OFF CACHE BOOL "" FORCE)
  set(GLSLANG_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
  set(BUILD_EXTERNAL OFF CACHE BOOL "" FORCE)
  FetchContent_Declare(glslang GIT_REPOSITORY https://github.com/KhronosGroup/glslang.git GIT_TAG 15.4.0 GIT_SHALLOW TRUE)
  # Vulkan Memory Allocator (header only; its CMake project isn't needed)
  FetchContent_Declare(vma GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git GIT_TAG v3.3.0
                       GIT_SHALLOW TRUE SOURCE_SUBDIR none)
  # volk: Vulkan entry points loaded at run time, from the system's libvulkan or (libadrenotools)
  # one that loads a GPU driver the user installed; tag matching the NDK's Vulkan headers
  FetchContent_Declare(volk GIT_REPOSITORY https://github.com/zeux/volk.git GIT_TAG vulkan-sdk-1.3.275.0
                       GIT_SHALLOW TRUE SOURCE_SUBDIR none)
  FetchContent_Declare(adrenotools GIT_REPOSITORY https://github.com/bylaws/libadrenotools.git
                       GIT_TAG 8fae8ce254dfc1344527e05301e43f37dea2df80 GIT_SUBMODULES_RECURSE TRUE)
  if(NOT CMAKE_ANDROID_ARCH_ABI)
    set(CMAKE_ANDROID_ARCH_ABI ${ANDROID_ABI})  # libadrenotools checks it
  endif()
  FetchContent_MakeAvailable(glslang vma volk adrenotools)
  # liblzma for 7z archives (LZMA/LZMA2) in the mod pack import: library only
  FetchContent_Declare(xz GIT_REPOSITORY https://github.com/tukaani-project/xz.git GIT_TAG v5.8.1 GIT_SHALLOW TRUE)
  foreach(opt XZ_NLS XZ_DOC XZ_TOOL_XZ XZ_TOOL_XZDEC XZ_TOOL_LZMADEC XZ_TOOL_LZMAINFO XZ_TOOL_SCRIPTS XZ_DOXYGEN)
    set(${opt} OFF CACHE BOOL "" FORCE)
  endforeach()
  set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
  FetchContent_MakeAvailable(xz)
  # libarchive's find_package(LibLZMA) takes these as found (no library file exists at configure time;
  # its try-compile checks then fail quietly: LZMA_API_STATIC is a Windows matter, the MT encoder unused)
  set(LIBLZMA_INCLUDE_DIR ${xz_SOURCE_DIR}/src/liblzma/api CACHE PATH "" FORCE)
  set(LIBLZMA_LIBRARY liblzma CACHE STRING "" FORCE)
  foreach(v LIBLZMA_HAS_AUTO_DECODER LIBLZMA_HAS_EASY_ENCODER LIBLZMA_HAS_LZMA_PRESET)
    set(${v} 1 CACHE INTERNAL "")
  endforeach()
  # mod pack import (runtime/src/android/archive.cpp): zip, RAR 4/5 and 7z archives, read only, no crypto
  FetchContent_Declare(libarchive GIT_REPOSITORY https://github.com/libarchive/libarchive.git GIT_TAG v3.7.7 GIT_SHALLOW TRUE)
  foreach(opt ENABLE_MBEDTLS ENABLE_NETTLE ENABLE_OPENSSL ENABLE_LIBB2 ENABLE_LZ4 ENABLE_LZO ENABLE_ZSTD ENABLE_BZip2
              ENABLE_LIBXML2 ENABLE_EXPAT ENABLE_PCREPOSIX ENABLE_PCRE2POSIX ENABLE_LIBGCC ENABLE_CNG ENABLE_TAR ENABLE_CPIO ENABLE_CAT
              ENABLE_UNZIP ENABLE_XATTR ENABLE_ACL ENABLE_ICONV ENABLE_TEST ENABLE_INSTALL ENABLE_COVERAGE ENABLE_WERROR)
    set(${opt} OFF CACHE BOOL "" FORCE)
  endforeach()
  set(ENABLE_ZLIB ON CACHE BOOL "" FORCE)
  set(ENABLE_LZMA ON CACHE BOOL "" FORCE)
  set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
  set(CMAKE_POLICY_VERSION_MINIMUM 3.5)  # libarchive 3.7 declares an older minimum, which CMake 4 refuses
  FetchContent_MakeAvailable(libarchive)
  unset(CMAKE_POLICY_VERSION_MINIMUM)

  file(GLOB VK_SOURCES CONFIGURE_DEPENDS runtime/src/vk/*.cpp)
  file(GLOB ANDROID_SOURCES CONFIGURE_DEPENDS runtime/src/android/*.cpp)
  # GameCube player models converted for the mod pack import (gcmodel.h; tiling through Cemu's address library)
  file(GLOB GCMODEL_SOURCES CONFIGURE_DEPENDS runtime/src/gcmodel/*.cpp)
  set_source_files_properties(${GX2_SOURCES} ${VK_SOURCES} ${ANDROID_SOURCES} ${SHARED_GFX_SOURCES} ${GCMODEL_SOURCES} PROPERTIES COMPILE_OPTIONS "-include;${CEMU_DIR}/cemu_shim.h")

  # the runtime's own shaders (runtime/shaders), compiled to SPIR-V here with the NDK's glslc: the
  # sources include the words as numbers ("<name>.inc"), so no shader is compiled at start
  file(GLOB GLSLC_DIRS LIST_DIRECTORIES true "${ANDROID_NDK}/shader-tools/*")
  find_program(GLSLC glslc HINTS ${GLSLC_DIRS} NO_CMAKE_FIND_ROOT_PATH REQUIRED)
  set(SPV_INCS)
  foreach(sh bc_decode.comp present.vert present.frag)
    set(out ${CMAKE_BINARY_DIR}/shaders/${sh}.inc)
    add_custom_command(OUTPUT ${out}
      COMMAND ${GLSLC} --target-env=vulkan1.1 --target-spv=spv1.3 -mfmt=num -o ${out} ${CMAKE_SOURCE_DIR}/runtime/shaders/${sh}
      DEPENDS runtime/shaders/${sh} VERBATIM)
    list(APPEND SPV_INCS ${out})
  endforeach()

  add_library(wwhd SHARED ${RUNTIME_SOURCES} runtime/src/audio_out_aaudio.cpp ${GX2_SOURCES} ${VK_SOURCES} ${ANDROID_SOURCES} ${GCMODEL_SOURCES}
                          ${SHARED_GFX_SOURCES} ${volk_SOURCE_DIR}/volk.c ${SPV_INCS})
  target_include_directories(wwhd PRIVATE runtime/include runtime/src runtime/src/gx2 ${vma_SOURCE_DIR}/include ${glslang_SOURCE_DIR}
                                          ${volk_SOURCE_DIR} ${CMAKE_BINARY_DIR}/shaders)
  # no Vulkan prototypes: every vk* call goes through volk's function pointers
  target_compile_definitions(wwhd PRIVATE VK_NO_PROTOTYPES VK_USE_PLATFORM_ANDROID_KHR VMA_STATIC_VULKAN_FUNCTIONS=0
                                      VMA_DYNAMIC_VULKAN_FUNCTIONS=1 VMA_VULKAN_VERSION=1001000)  # Vulkan 1.1 entry points only
  if(WWHD_PUBLIC_RELEASE)
    target_compile_definitions(wwhd PRIVATE WWHD_PUBLIC_RELEASE=1)
  endif()
  target_compile_options(wwhd PRIVATE -Wall -Wno-unused-function -Wno-unused-variable -Wno-unused-private-field
                                      -Wno-nullability-completeness -Wno-mismatched-tags -ffp-contract=off)
  # 16 KiB page devices (Android 15+); -Bsymbolic: calls between the recompiled functions bind
  # directly instead of through the PLT (their names stay exported for backtraces)
  target_link_options(wwhd PRIVATE "-Wl,-z,max-page-size=16384" "-Wl,-Bsymbolic")
  target_link_libraries(wwhd PRIVATE cemu_latte glslang glslang-default-resource-limits SPIRV
                                     adrenotools archive_static android nativewindow log aaudio z dl)
  target_include_directories(wwhd PRIVATE ${libarchive_SOURCE_DIR}/libarchive)
  # libadrenotools' hooks, loaded into the driver's linker namespace (packaged next to libwwhd.so)
  add_dependencies(wwhd main_hook hook_impl)
  foreach(t main_hook hook_impl)
    target_link_options(${t} PRIVATE "-Wl,-z,max-page-size=16384")  # 16 KiB page devices, as libwwhd.so
  endforeach()
  if(NOT WWHD_DEVICE_RECOMP)
    target_link_libraries(wwhd PRIVATE gamecode)
  endif()

  # ---- the randomizer (TWWHD Randomizer, runtime/third_party/twwhd_rando/WWHD.md): libwwhdrando.so,
  # loaded only by the app's ":rando" process (RandoService); its data goes in as assets/rando (build.gradle)
  set(RANDO_DIR ${CMAKE_SOURCE_DIR}/runtime/third_party/twwhd_rando)
  file(GLOB RANDO_SOURCES CONFIGURE_DEPENDS
    ${RANDO_DIR}/*.cpp ${RANDO_DIR}/command/*.cpp ${RANDO_DIR}/customizer/*.cpp ${RANDO_DIR}/filetypes/*.cpp
    ${RANDO_DIR}/filetypes/util/*.cpp ${RANDO_DIR}/filetypes/shared/*.cpp ${RANDO_DIR}/filetypes/texture/*.cpp
    ${RANDO_DIR}/filetypes/subfiles/*.cpp ${RANDO_DIR}/logic/*.cpp ${RANDO_DIR}/logic/flatten/*.cpp ${RANDO_DIR}/seedgen/*.cpp
    ${RANDO_DIR}/utility/*.cpp ${RANDO_DIR}/libs/yaml.cpp ${RANDO_DIR}/libs/hash-library/sha1.cpp ${RANDO_DIR}/libs/hash-library/sha256.cpp
    ${RANDO_DIR}/libs/tinyxml2/tinyxml2.cpp ${RANDO_DIR}/libs/base64pp/base64pp/base64pp.cpp ${RANDO_DIR}/libs/yaml-cpp/src/*.cpp
    ${RANDO_DIR}/libs/yaml-cpp/src/contrib/*.cpp)
  set(GIT_TAG 1.2.0)  # the release it is (permalinks and seeds match the PC randomizer 1.2.0)
  set(SEED_KEY "")
  configure_file(${RANDO_DIR}/version.hpp.in ${CMAKE_BINARY_DIR}/rando/version.hpp @ONLY)
  configure_file(${RANDO_DIR}/keys/keys.hpp.in ${CMAKE_BINARY_DIR}/rando/keys/keys.hpp @ONLY)
  add_library(wwhdrando SHARED ${RANDO_SOURCES} runtime/src/rando/rando_jni.cpp)
  set_target_properties(wwhdrando PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON)
  target_include_directories(wwhdrando PRIVATE ${RANDO_DIR} ${CMAKE_BINARY_DIR}/rando ${RANDO_DIR}/libs/yaml-cpp/include
                                               ${RANDO_DIR}/libs/base64pp/base64pp/include)
  string(LENGTH "${RANDO_DIR}/" RANDO_PATH_SIZE)
  target_compile_definitions(wwhdrando PRIVATE WWHD_APP SOURCE_PATH_SIZE=${RANDO_PATH_SIZE} YAML_CPP_STATIC_DEFINE)
  target_compile_options(wwhdrando PRIVATE -fexceptions -frtti -Wno-everything)
  target_link_options(wwhdrando PRIVATE "-Wl,-z,max-page-size=16384")
  target_link_libraries(wwhdrando PRIVATE z log)

  # ---- the licenses of everything in the APK, shown in the app (About and licenses)
  set(LIC_PARTS
    "This project|MPL-2.0|${CMAKE_SOURCE_DIR}/LICENSE"
    "Cemu (GPU address library, shader decompiler, PPC assembler and graphic pack patches, in runtime/third_party/cemu)|MPL-2.0 (same license text as this project)|"
    "fast_float (number parsing in Cemu's expression parser)|MIT or Apache-2.0 (notice in runtime/third_party/cemu/Common/unix/fast_float.h)|"
    "{fmt}|MIT|${CMAKE_SOURCE_DIR}/runtime/third_party/fmt/LICENSE"
    "Zstandard (zstd: .wua archives, save states)|BSD-3-Clause|${CMAKE_SOURCE_DIR}/runtime/third_party/zstd/LICENSE"
    "glslang|BSD-3-Clause and others|${glslang_SOURCE_DIR}/LICENSE.txt"
    "Vulkan Memory Allocator|MIT|${vma_SOURCE_DIR}/LICENSE.txt"
    "volk|MIT|${volk_SOURCE_DIR}/LICENSE.md"
    "libadrenotools|BSD-2-Clause|${adrenotools_SOURCE_DIR}/LICENSE"
    "libarchive (mod pack archives: zip, RAR, 7z)|BSD-2-Clause|${libarchive_SOURCE_DIR}/COPYING"
    "liblzma from XZ Utils (7z mod pack archives)|0BSD|${xz_SOURCE_DIR}/COPYING.0BSD"
    "linkernsbypass (part of libadrenotools)|BSD-2-Clause|${adrenotools_SOURCE_DIR}/lib/linkernsbypass/LICENSE"
    "TWWHD Randomizer (Game > Randomizer, in runtime/third_party/twwhd_rando)|MIT|${RANDO_DIR}/LICENSE.txt"
    "yaml-cpp (the randomizer's settings and logic files)|MIT|${RANDO_DIR}/libs/yaml-cpp/LICENSE"
    "TinyXML-2 (part of the randomizer)|Zlib|${RANDO_DIR}/libs/tinyxml2/LICENSE.txt"
    "base64pp (the randomizer's permalinks)|MIT|${RANDO_DIR}/libs/base64pp/LICENSE"
    "BS::thread_pool (part of the randomizer)|MIT|${RANDO_DIR}/libs/thread-pool/LICENSE.txt"
    "hash-library (part of the randomizer)|Zlib|")
  set(WWHD_LLVM_DIR ${CMAKE_SOURCE_DIR}/build/llvm/install CACHE PATH "LLVM built for Android (tools/android/build-llvm.sh)")
  if(EXISTS ${WWHD_LLVM_DIR}/lib/cmake/llvm/LLVMConfig.cmake)
    find_package(LLVM REQUIRED CONFIG PATHS ${WWHD_LLVM_DIR}/lib/cmake/llvm NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
    message(STATUS "on-device recompiler: LLVM ${LLVM_PACKAGE_VERSION} from ${WWHD_LLVM_DIR}")
    set(WWHD_LLVM_COMPONENTS core bitreader passes aarch64codegen aarch64desc aarch64info target mc)
    if(WWHD_DEVICE_RECOMP)
      list(APPEND WWHD_LLVM_COMPONENTS orcjit jitlink orcshared orctargetprocess executionengine)
    endif()
    llvm_map_components_to_libnames(WWHD_LLVM_LIBS ${WWHD_LLVM_COMPONENTS})
    target_sources(wwhd PRIVATE runtime/src/recomp/program.cpp runtime/src/recomp/decode.cpp runtime/src/recomp/emit.cpp
                                runtime/src/recomp/locals.cpp runtime/src/recomp/compile.cpp ${CMAKE_BINARY_DIR}/recomp_data.S)
    set_source_files_properties(runtime/src/recomp/emit.cpp runtime/src/recomp/locals.cpp runtime/src/recomp/compile.cpp PROPERTIES
                                INCLUDE_DIRECTORIES "${LLVM_INCLUDE_DIRS}")
    target_compile_definitions(wwhd PRIVATE WWHD_RECOMP=1 WWHD_RELEASE_MAPS=1)  # release.cpp: the EUR and JPN address maps
    target_link_libraries(wwhd PRIVATE ${WWHD_LLVM_LIBS})
    list(APPEND LIC_PARTS "LLVM (the on-device recompiler) and libc++|Apache-2.0 with LLVM Exceptions|${WWHD_LLVM_DIR}/LICENSE.TXT")
    # ops.c as LLVM bitcode (the instruction semantics the recompiler inlines), embedded with the hook lists
    set(OPS_BC ${CMAKE_BINARY_DIR}/ops.bc)
    add_custom_command(OUTPUT ${OPS_BC}
      # relative paths: the bitcode records the source file name as given (embedded in the APK)
      COMMAND ${CMAKE_C_COMPILER} --target=aarch64-linux-android30 -O2 -ffp-contract=off -fno-strict-aliasing -emit-llvm
              -c runtime/src/recomp/ops.c -Iruntime/include -o ${OPS_BC}
      WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
      DEPENDS runtime/src/recomp/ops.c runtime/include/ppc.h VERBATIM)
    set(HOOKS_DIR ${CMAKE_SOURCE_DIR}/tools/recomp)
    file(WRITE ${CMAKE_BINARY_DIR}/recomp_data.S
"    .section .rodata
    .balign 16
    .global wwhd_ops_bc, wwhd_ops_bc_end, wwhd_hooks, wwhd_hooks_end, wwhd_release_eur, wwhd_release_eur_end
    .global wwhd_release_jpn, wwhd_release_jpn_end, wwhd_hot, wwhd_hot_end
wwhd_hot: .incbin \"${HOOKS_DIR}/hot_functions.txt\"
wwhd_hot_end:
wwhd_release_eur: .incbin \"${HOOKS_DIR}/release_eur.txt\"
wwhd_release_eur_end:
wwhd_release_jpn: .incbin \"${HOOKS_DIR}/release_jpn.txt\"
wwhd_release_jpn_end:
wwhd_ops_bc: .incbin \"${OPS_BC}\"
wwhd_ops_bc_end:
wwhd_hooks:
    .incbin \"${HOOKS_DIR}/hooks.txt\"
    .byte 10
    .incbin \"${HOOKS_DIR}/hooks_climb.txt\"
    .byte 10
    .incbin \"${HOOKS_DIR}/hooks_mods.txt\"
    .byte 10
    .incbin \"${HOOKS_DIR}/hooks_aspect.txt\"
    .byte 10
    .incbin \"${HOOKS_DIR}/hooks_perf.txt\"
    .byte 10
    .incbin \"${HOOKS_DIR}/hooks_resmgr.txt\"
wwhd_hooks_end:
")
    set_source_files_properties(${CMAKE_BINARY_DIR}/recomp_data.S PROPERTIES OBJECT_DEPENDS
      "${OPS_BC};${HOOKS_DIR}/hooks.txt;${HOOKS_DIR}/hooks_climb.txt;${HOOKS_DIR}/hooks_mods.txt;${HOOKS_DIR}/hooks_aspect.txt;${HOOKS_DIR}/hooks_perf.txt;${HOOKS_DIR}/hooks_resmgr.txt;${HOOKS_DIR}/release_eur.txt;${HOOKS_DIR}/release_jpn.txt;${HOOKS_DIR}/hot_functions.txt")

    if(WWHD_DEVICE_RECOMP)
      # The game code is linked at startup (loader.cpp). The game functions the runtime calls
      # directly (f_X, f_X_orig in hooks and mods) become stubs jumping through slots the loader fills.
      set(JIT_NAMES "")
      foreach(f ${RUNTIME_SOURCES})
        file(READ ${f} text)
        string(REGEX MATCHALL "f_[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F](_orig)?" found "${text}")
        list(APPEND JIT_NAMES ${found})
      endforeach()
      set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${RUNTIME_SOURCES})
      list(REMOVE_DUPLICATES JIT_NAMES)
      list(SORT JIT_NAMES)
      list(LENGTH JIT_NAMES JIT_COUNT)
      set(stubs "// generated by CMakeLists.txt (WWHD_DEVICE_RECOMP): see runtime/src/recomp/loader.cpp\n#include \"ppc.h\"\n\nextern \"C\" {\n")
      string(APPEND stubs "PpcFunc wwhd_jit_slots[${JIT_COUNT}];\nextern const unsigned wwhd_jit_count = ${JIT_COUNT};\nextern const char* const wwhd_jit_names[] = {\n")
      foreach(n ${JIT_NAMES})
        string(APPEND stubs "    \"${n}\",\n")
      endforeach()
      string(APPEND stubs "};\n")
      set(i 0)
      foreach(n ${JIT_NAMES})
        string(APPEND stubs "void ${n}(Cpu* c) { [[clang::musttail]] return wwhd_jit_slots[${i}](c); }\n")
        math(EXPR i "${i} + 1")
      endforeach()
      string(APPEND stubs "}\n")
      file(CONFIGURE OUTPUT ${CMAKE_BINARY_DIR}/runtime_stubs.cpp CONTENT "${stubs}" @ONLY)
      target_sources(wwhd PRIVATE runtime/src/recomp/loader.cpp ${CMAKE_BINARY_DIR}/runtime_stubs.cpp)
      # the code cache's version: the recompiler's sources, the instruction semantics (ops.c, the
      # Cpu layout in ppc.h) and LLVM; the hook lists are hashed at runtime (loader.cpp)
      set(RECOMP_VERSION_FILES runtime/src/recomp/compile.cpp runtime/src/recomp/compile.h runtime/src/recomp/decode.cpp
          runtime/src/recomp/decode.h runtime/src/recomp/emit.cpp runtime/src/recomp/emit.h runtime/src/recomp/program.cpp
          runtime/src/recomp/program.h runtime/src/recomp/locals.cpp runtime/src/recomp/locals.h runtime/src/recomp/ops.c
          runtime/include/ppc.h)
      list(TRANSFORM RECOMP_VERSION_FILES PREPEND ${CMAKE_SOURCE_DIR}/)
      string(REPLACE ";" "|" files_arg "${RECOMP_VERSION_FILES}")
      add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/recomp_version.h
        COMMAND ${CMAKE_COMMAND} -DOUT=${CMAKE_BINARY_DIR}/recomp_version.h "-DFILES=${files_arg}" "-DEXTRA=LLVM ${LLVM_PACKAGE_VERSION}"
                -P ${CMAKE_SOURCE_DIR}/cmake/recomp_version.cmake
        DEPENDS ${RECOMP_VERSION_FILES} ${CMAKE_SOURCE_DIR}/cmake/recomp_version.cmake VERBATIM)
      set_source_files_properties(runtime/src/recomp/loader.cpp PROPERTIES
        INCLUDE_DIRECTORIES "${LLVM_INCLUDE_DIRS};${CMAKE_BINARY_DIR}" OBJECT_DEPENDS ${CMAKE_BINARY_DIR}/recomp_version.h)
      target_compile_definitions(wwhd PRIVATE WWHD_DEVICE_RECOMP=1)
    endif()
  elseif(WWHD_DEVICE_RECOMP)
    message(FATAL_ERROR "WWHD_DEVICE_RECOMP needs LLVM in ${WWHD_LLVM_DIR} (tools/android/build-llvm.sh)")
  endif()

  set(lic "TLoZ:TWW HD Recompiled: an unofficial fan project, not affiliated with or endorsed by Nintendo.\n")
  if(WWHD_DEVICE_RECOMP)
    string(APPEND lic "This app contains no code or data from the game; it works with your own copy, from which it extracts the files and builds the game code on this device.\n")
  endif()
  string(APPEND lic "Source code: https://github.com/GreenNaugahyde/ZeldaWWHDRecompAndroid (based on https://github.com/ZeldaWWHDRecomp/ZeldaWWHDRecomp)\n\nThis app contains:\n")
  foreach(part ${LIC_PARTS})
    string(REPLACE "|" ";" f "${part}")
    list(GET f 0 what)
    list(GET f 1 license)
    string(APPEND lic "- ${what}: ${license}\n")
  endforeach()
  foreach(part ${LIC_PARTS})
    string(REPLACE "|" ";" f "${part}")
    list(GET f 0 what)
    list(LENGTH f n)
    if(n GREATER 2)
      list(GET f 2 file)
      if(file)
        if(NOT EXISTS ${file})
          message(FATAL_ERROR "license file missing: ${file}")
        endif()
        file(READ ${file} text)
        string(APPEND lic "\n\n==== ${what} ====\n\n${text}")
      endif()
    endif()
  endforeach()
  file(CONFIGURE OUTPUT ${CMAKE_BINARY_DIR}/licenses.txt CONTENT "${lic}" @ONLY)
  file(WRITE ${CMAKE_BINARY_DIR}/licenses.S
"    .section .rodata
    .global wwhd_licenses, wwhd_licenses_end
wwhd_licenses: .incbin \"${CMAKE_BINARY_DIR}/licenses.txt\"
wwhd_licenses_end:
")
  set_source_files_properties(${CMAKE_BINARY_DIR}/licenses.S PROPERTIES OBJECT_DEPENDS ${CMAKE_BINARY_DIR}/licenses.txt)
  target_sources(wwhd PRIVATE ${CMAKE_BINARY_DIR}/licenses.S)

else()
  message(FATAL_ERROR "supported targets: macOS (Metal) and Android (Vulkan); see README.md")
endif()
target_link_libraries(wwhd PRIVATE zstd_dec)
