cmake_minimum_required(VERSION 3.20)

# Windows: pass the 64-bit-hosted clang-cl.exe (Tools\Llvm\x64\bin) by full path, as in
# BUILDING.md. Visual Studio also ships a 32-bit-hosted one, and a bare "clang-cl" on PATH
# can pick it.

if (APPLE)
    set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
endif()

project(SnPRecompiled LANGUAGES C CXX)

# The default "Developer PowerShell for VS 2022" is set up for 32-bit: linking
# then fails with "machine type x86 conflicts with x64" (BUILDING.md).
if (WIN32 AND "$ENV{VSCMD_ARG_TGT_ARCH}" STREQUAL "x86")
    message(FATAL_ERROR "This Developer PowerShell is set up for 32-bit (x86). Switch it to 64-bit with:\n"
        "  & \"$ENV{VSINSTALLDIR}Common7\\Tools\\Launch-VsDevShell.ps1\" -Arch amd64 -HostArch amd64\n"
        "then cd back to this folder and run the cmake commands again.")
endif()
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-everything /W4")
endif()

# Keep the build machine's folders (e.g. the Windows user name) out of the
# binaries: source paths that end up in them, through __FILE__ in asserts and
# log messages or in debug info, are made relative to the repo root. Both
# slash styles are mapped, since Windows tools pass either.
file(TO_NATIVE_PATH "${CMAKE_SOURCE_DIR}" SNP_SOURCE_DIR_NATIVE)
foreach (prefix IN ITEMS "${CMAKE_SOURCE_DIR}" "${SNP_SOURCE_DIR_NATIVE}")
    if (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_SIMULATE_ID STREQUAL "MSVC")
        add_compile_options("/clang:-ffile-prefix-map=${prefix}=.")
    elseif (CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
        add_compile_options("-ffile-prefix-map=${prefix}=.")
    endif()
endforeach()

# Opt out of constexpr mutex constructor on windows to prevent vcredist issues
if (WIN32)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR")
endif()

if (APPLE)
    enable_language(OBJC OBJCXX)
endif()

if (CMAKE_SYSTEM_NAME MATCHES "Linux")
    option(RECOMP_FLATPAK "Configure the build for Flatpak compatibility." OFF)
endif()



# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
    cmake_policy(SET CMP0135 NEW)
endif()

set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

if (WIN32)
    set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/lib/")
endif()

set(RT64_STATIC TRUE)
set(RT64_SDL_WINDOW_VULKAN TRUE)
add_compile_definitions(
    HLSL_CPU
    __PRFCHWINTRIN_H
)

if (RECOMP_FLATPAK)
    add_compile_definitions(RECOMP_FLATPAK)
endif()

# Host-built tools and pre-generated sources when cross-compiling (Android).
include(${CMAKE_SOURCE_DIR}/cmake/CrossCompile.cmake)
# SDL2, FreeType and nfd for Android.
include(${CMAKE_SOURCE_DIR}/cmake/Android.cmake)

# Local RT64 fixes (lib-patches/rt64/), applied to the submodule at configure time.
include(${CMAKE_SOURCE_DIR}/cmake/ApplyLibPatches.cmake)
snp_apply_lib_patches(rt64)
# plume (RT64's rendering layer) is a submodule inside rt64.
snp_apply_lib_patches(plume lib/rt64/src/contrib/plume)

# Linux: the ROM picker is the desktop's own, through xdg-desktop-portal (D-Bus),
# not GTK's. GTK may only be used from the thread that started it, and the
# picker opens from the UI thread: it crashed inside GTK as it opened, now and
# then (issues #5 and #9).
if(UNIX AND NOT APPLE)
    set(NFD_PORTAL ON CACHE BOOL "" FORCE)
endif()
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)

set(BUILD_SHARED_LIBS OFF)

# Local runtime and recompiler fixes (lib-patches/N64ModernRuntime/, lib-patches/N64Recomp/),
# applied the same way. N64Recomp is a submodule inside N64ModernRuntime; its fixes change the
# generated code, so RecompiledFuncs/ must be generated with the N64Recomp this build makes
# (BUILDING.md).
snp_apply_lib_patches(N64ModernRuntime)
snp_apply_lib_patches(N64Recomp lib/N64ModernRuntime/N64Recomp)
file(GLOB SNP_N64RECOMP_PATCHES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/lib-patches/N64Recomp/*.patch)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)


target_include_directories(rt64 PRIVATE ${CMAKE_BINARY_DIR}/rt64/src)

# RecompiledFuncs - Library containing the primary recompiler output
add_library(RecompiledFuncs STATIC)

# clang-cl silently drops bare GNU-style "-include <file>" (two tokens) with just a warning,
# then misparses the orphaned path token as a second input source file, which is a hard error
# ("cannot specify /Fo... when compiling multiple source files") rather than a missing include.
# The MSVC-native spelling is a single "/FI<file>" token. -falign-functions has no MSVC/clang-cl
# equivalent and is dropped too, but only as a (harmless, alignment-only) warning, so it's left
# as-is rather than special-cased.
if(MSVC)
    set(SNP_RECOMP_HOOKS_FORCE_INCLUDE "SHELL:/FI${CMAKE_SOURCE_DIR}/include/snp_recomp_hooks.h")
else()
    set(SNP_RECOMP_HOOKS_FORCE_INCLUDE "SHELL:-include ${CMAKE_SOURCE_DIR}/include/snp_recomp_hooks.h")
endif()
target_compile_options(RecompiledFuncs PRIVATE
    # -Wno-unused-but-set-variable
    -fno-strict-aliasing
    -Wno-unused-variable
    -Wno-implicit-function-declaration
    -falign-functions=16
    # Declarations for the [[patches.hook]] text in snp.jp.toml.
    ${SNP_RECOMP_HOOKS_FORCE_INCLUDE}
)

target_include_directories(RecompiledFuncs PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
)

# CONFIGURE_DEPENDS: regenerating can add a new funcs_N.c (more functions
# spill into another file), which a plain configure-time glob never sees --
# the build then links against a library missing that file's functions.
file(GLOB FUNC_C_SOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/RecompiledFuncs/*.c)
file(GLOB FUNC_CXX_SOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/RecompiledFuncs/*.cpp)

if (NOT FUNC_C_SOURCES AND NOT FUNC_CXX_SOURCES)
    # The recompiled game code is generated from the ROM, which this repository cannot ship, so
    # a fresh clone has none of it yet. Keep the target valid with an empty placeholder so the
    # renderer, runtime, frontend and patches still configure and build; only the game executable
    # needs the generated sources. Run N64Recomp snp.jp.toml to produce them.
    message(STATUS "RecompiledFuncs/ is empty - building without the recompiled game code.")
    file(WRITE ${CMAKE_BINARY_DIR}/recompiled_funcs_placeholder.c "")
    set(FUNC_C_SOURCES ${CMAKE_BINARY_DIR}/recompiled_funcs_placeholder.c)
endif()

target_sources(RecompiledFuncs PRIVATE ${FUNC_C_SOURCES} ${FUNC_CXX_SOURCES})

# Regenerate RecompiledFuncs/ when the N64Recomp config or the symbol file changes, so a
# rebuild after a pull can't keep stale hooks. Only once the code has been generated by hand
# the first time, and with the ROM in place. BYPRODUCTS makes Ninja recompile the regenerated
# files in the same build; recomp_overlays.inl is included by src/main/register_overlays.cpp,
# so the executable waits for the regeneration too (below, after it is defined).
set(SNP_ROM_FILE "${CMAKE_SOURCE_DIR}/Tsumi to Batsu - Hoshi no Keishousha (Japan).z64")
# Not when cross-compiling (Android): N64RecompCLI is then built for the target and can't run
# here, and cmake/CrossCompile.cmake leaves the ROM-derived sources as they are; regenerate
# them with a desktop build.
if (EXISTS "${SNP_ROM_FILE}" AND (FUNC_C_SOURCES OR FUNC_CXX_SOURCES)
        AND NOT FUNC_C_SOURCES MATCHES "recompiled_funcs_placeholder" AND NOT CMAKE_CROSSCOMPILING)
    add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/recompiled_funcs.stamp
        COMMAND $<TARGET_FILE:N64RecompCLI> snp.jp.toml
        COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_BINARY_DIR}/recompiled_funcs.stamp
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        DEPENDS ${CMAKE_SOURCE_DIR}/snp.jp.toml
                ${CMAKE_SOURCE_DIR}/SnPSyms/snp.jp.syms.toml
                ${SNP_N64RECOMP_PATCHES}
                N64RecompCLI
        BYPRODUCTS ${FUNC_C_SOURCES} ${FUNC_CXX_SOURCES}
                ${CMAKE_SOURCE_DIR}/RecompiledFuncs/recomp_overlays.inl
                ${CMAKE_SOURCE_DIR}/RecompiledFuncs/funcs.h
        COMMENT "Regenerating RecompiledFuncs/ from snp.jp.toml"
        VERBATIM)
    add_custom_target(RecompiledFuncsGen DEPENDS ${CMAKE_BINARY_DIR}/recompiled_funcs.stamp)
    add_dependencies(RecompiledFuncs RecompiledFuncsGen)
endif()

# PatchesLib - recompiled patches (patches/*.c, e.g. the per-frame UI pump at the frame swap)
add_library(PatchesLib STATIC)
target_compile_options(PatchesLib PRIVATE -fno-strict-aliasing -Wno-unused-variable -Wno-implicit-function-declaration -falign-functions=16)
target_include_directories(PatchesLib PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
)

# The patch pipeline: patches/*.c is cross-compiled to a MIPS ELF, then run
# back through N64Recomp (patches.toml) to produce patches.c/patches_bin.c.
# Without patch sources or patches.toml, PatchesLib builds as an empty
# placeholder so the project still configures.
file(GLOB PATCH_SOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/patches/*.c)
if (NOT PATCH_SOURCES OR NOT EXISTS ${CMAKE_SOURCE_DIR}/patches.toml)
    message(STATUS "patches/ has no sources yet (or patches.toml is missing) - building PatchesLib as a placeholder only.")
    file(WRITE ${CMAKE_BINARY_DIR}/patcheslib_placeholder.c "")
    target_sources(PatchesLib PRIVATE ${CMAKE_BINARY_DIR}/patcheslib_placeholder.c)
else()
    # patches.c is generated by N64Recomp patches.toml from patches/patches.elf
    # patches_bin.c is generated by file_to_c from patches/patches.bin (embed)
    target_sources(PatchesLib PRIVATE ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c)
    set_source_files_properties(${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c PROPERTIES COMPILE_FLAGS -fno-strict-aliasing)
endif()

# Regenerating the patches needs N64Recomp and the MIPS toolchain on the build machine; a
# cross-compiled build uses the ones a desktop build generated (cmake/CrossCompile.cmake).
if (PATCH_SOURCES AND EXISTS ${CMAKE_SOURCE_DIR}/patches.toml AND NOT CMAKE_CROSSCOMPILING)
    add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/patches/patches.bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/recomp_overlays.inl ${CMAKE_SOURCE_DIR}/RecompiledPatches/funcs.h
        COMMAND $<TARGET_FILE:N64RecompCLI> patches.toml
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.elf N64RecompCLI
    )

    # patches.elf is built by the Makefile in patches/, with clang targeting MIPS.
    # Windows note: neither Visual Studio's bundled "C++ Clang Compiler for
    # Windows" nor the official llvm.org Windows installer builds include the
    # MIPS backend (confirmed empirically -- `clang -print-targets` lists
    # aarch64/arm/x86/riscv/wasm/bpf/nvptx but no mips on either). Ubuntu's
    # packaged clang does have it (this is how this pipeline was verified
    # end-to-end originally), so on Windows this routes the one `make` step
    # through WSL instead of failing on a MIPS-less native clang. `wsl.exe`
    # invoked with a Windows WORKING_DIRECTORY automatically translates it to
    # the equivalent /mnt/c/... path, so no path translation is needed here.
    # This depends on the build machine, not the target, hence CMAKE_HOST_WIN32.
    if (CMAKE_HOST_WIN32)
        set(PATCHES_C_COMPILER "clang" CACHE STRING "C compiler used to build the game patches (name only -- resolved inside WSL on Windows)")
        set(PATCHES_LD "ld.lld" CACHE STRING "Linker used to build the game patches (name only -- resolved inside WSL on Windows)")
        set(PATCHES_MAKE_COMMAND wsl.exe make)
    else()
        set(PATCHES_C_COMPILER "clang" CACHE STRING "C compiler used to build the game patches")
        set(PATCHES_LD "ld.lld" CACHE STRING "Linker used to build the game patches")
        set(PATCHES_MAKE_COMMAND make)
    endif()
    file(GLOB PATCH_HEADERS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/patches/*.h)
    add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/patches/patches.elf
        COMMAND ${PATCHES_MAKE_COMMAND} CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD}
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/patches
        DEPENDS ${PATCH_SOURCES} ${PATCH_HEADERS} ${CMAKE_SOURCE_DIR}/patches/Makefile ${CMAKE_SOURCE_DIR}/patches/patches.ld ${CMAKE_SOURCE_DIR}/patches/syms.ld
    )

    # patches.bin is embedded in the executable.
    add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
        COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin snp_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
        DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin file_to_c
    )
endif()


# Main executable. On Android it's libmain.so instead, which SDLActivity loads and
# whose main() it calls.
if (ANDROID)
    add_library(SnPRecompiled SHARED)
    set_target_properties(SnPRecompiled PROPERTIES OUTPUT_NAME main)
else()
    add_executable(SnPRecompiled)
endif()

# The frontend (src/main), the game-specific fixes (src/game) and the
# recompiled RSP audio microcode (rsp/, generated from the ROM). If none of
# these exist (e.g. a bare checkout), a placeholder target is built instead
# so the rest of the configuration can still be checked.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS
    ${CMAKE_SOURCE_DIR}/src/main/*.cpp
    ${CMAKE_SOURCE_DIR}/src/game/*.cpp
    ${CMAKE_SOURCE_DIR}/rsp/*.cpp
)

# src/main/register_overlays.cpp #includes RecompiledFuncs/recomp_overlays.inl,
# which (like the rest of RecompiledFuncs/) doesn't exist yet on a fresh
# clone before the ROM is in place and N64Recomp has been run -- see the
# RecompiledFuncs placeholder handling above. Gate its real content behind
# this so that case still configures and builds (as a no-op) rather than
# failing on a missing header.
if (EXISTS ${CMAKE_SOURCE_DIR}/RecompiledFuncs/recomp_overlays.inl)
    target_compile_definitions(SnPRecompiled PRIVATE SNP_HAS_RECOMPILED_FUNCS)
    if (TARGET RecompiledFuncsGen)
        add_dependencies(SnPRecompiled RecompiledFuncsGen)
    endif()
endif()

if (NOT SOURCES)
    message(STATUS "src/ and rsp/ are empty - building SnPRecompiled as a placeholder only.")
    file(WRITE ${CMAKE_BINARY_DIR}/snp_placeholder.cpp
        "int main() { return 0; } // No game source found -- see BUILDING.md.\n")
    set(SOURCES ${CMAKE_BINARY_DIR}/snp_placeholder.cpp)
endif()

target_include_directories(SnPRecompiled PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
    ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/thirdparty/sse2neon
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/hlslpp/include
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/dxc/inc
    ${CMAKE_SOURCE_DIR}/lib/rt64/src
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/rhi
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/render
    ${CMAKE_SOURCE_DIR}/lib/RecompFrontend/recompinput/include
    ${CMAKE_SOURCE_DIR}/lib/RecompFrontend/recompui/include
    ${CMAKE_SOURCE_DIR}/lib/RecompFrontend/recompui/src
    ${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/nativefiledialog-extended/src/include
    ${CMAKE_SOURCE_DIR}/lib/SlotMap
    ${CMAKE_CURRENT_BINARY_DIR}
)

# Generate icon_bytes.c from the app icon PNG (the window icon, set in
# create_window in src/main/main.cpp).
if (EXISTS ${CMAKE_SOURCE_DIR}/icons/app.png)
    add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
        COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/app.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
        DEPENDS ${CMAKE_SOURCE_DIR}/icons/app.png
    )
    target_sources(SnPRecompiled PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c)
    target_compile_definitions(SnPRecompiled PRIVATE SNP_HAS_APP_ICON)
endif()

if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
    target_compile_options(SnPRecompiled PRIVATE
        -march=nehalem
        -fno-strict-aliasing
        -fms-extensions
    )
else()
    target_compile_options(SnPRecompiled PRIVATE
        -fno-strict-aliasing
        -fms-extensions
    )
endif()

if (MSVC)
    # Disable identical code folding, since this breaks mod function patching as multiple functions can get merged into one.
    target_link_options(SnPRecompiled PRIVATE /OPT:NOICF)
elseif (APPLE)
    # Use a wrapper around ld64 that respects segprot's `max_prot` value in order
    # to make our executable memory writable (required for mod function patching)
    target_link_options(SnPRecompiled PRIVATE
        "-fuse-ld=${CMAKE_SOURCE_DIR}/.github/macos/ld64"
    )
endif()

if (WIN32)
    include(FetchContent)

    if (DEFINED ENV{SDL2_VERSION})
        set(SDL2_VERSION $ENV{SDL2_VERSION})
    else()
        set(SDL2_VERSION "2.30.3")
    endif()

    # Fetch SDL2 on windows
    FetchContent_Declare(
        sdl2
        URL https://github.com/libsdl-org/SDL/releases/download/release-${SDL2_VERSION}/SDL2-devel-${SDL2_VERSION}-VC.zip
    )
    FetchContent_MakeAvailable(sdl2)
    target_include_directories(SnPRecompiled PRIVATE
        ${sdl2_SOURCE_DIR}/include
    )
    target_link_directories(SnPRecompiled PRIVATE
        ${sdl2_SOURCE_DIR}/lib/x64
    )

    # Copy SDL2 and dxc DLLs to output folder as post build step
    add_custom_command(TARGET SnPRecompiled POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${sdl2_SOURCE_DIR}/lib/x64/SDL2.dll"
            "${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxil.dll"
            "${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxcompiler.dll"
            $<TARGET_FILE_DIR:SnPRecompiled>)

    set_target_properties(
        SnPRecompiled
        PROPERTIES
            LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE"
            LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
            LINK_FLAGS_RELWITHDEBINFO  "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
            LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
    )

    # The .exe icon: icons/app.res, a precompiled resource (icons/app.ico as
    # icon 1) handed straight to the linker, which accepts .res files. This
    # skips CMake's clang-cl + rc.exe resource rule, which fails here.
    # Regenerate with: llvm-rc /FO app.res <script containing: 1 ICON "app.ico">
    if (EXISTS ${CMAKE_SOURCE_DIR}/icons/app.res)
        target_link_libraries(SnPRecompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.res)
    endif()
    target_link_libraries(SnPRecompiled PRIVATE SDL2 Winmm.lib Urlmon.lib)
endif()

if (APPLE)
    find_package(SDL2 REQUIRED)
    target_include_directories(SnPRecompiled PRIVATE ${SDL2_INCLUDE_DIRS})

    set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
    set(THREADS_PREFER_PTHREAD_FLAG TRUE)
    find_package(Threads REQUIRED)

    target_link_libraries(SnPRecompiled PRIVATE ${CMAKE_DL_LIBS} Threads::Threads SDL2::SDL2)

    include(${CMAKE_SOURCE_DIR}/.github/macos/apple_bundle.cmake)
endif()

if (CMAKE_SYSTEM_NAME MATCHES "Linux")
    find_package(SDL2 REQUIRED)

    add_compile_definitions("PLUME_SDL_VULKAN_ENABLED")
    add_compile_definitions("RT64_SDL_WINDOW_VULKAN")

    message(STATUS "SDL2_FOUND = ${SDL2_FOUND}")
    message(STATUS "SDL2_INCLUDE_DIRS = ${SDL2_INCLUDE_DIRS}")

    target_include_directories(SnPRecompiled PRIVATE ${SDL2_INCLUDE_DIRS})

    find_package(Freetype REQUIRED)

    message(STATUS "FREETYPE_FOUND = ${FREETYPE_FOUND}")
    message(STATUS "FREETYPE_INCLUDE_DIRS = ${FREETYPE_INCLUDE_DIRS}")
    message(STATUS "FREETYPE_LIBRARIES = ${FREETYPE_LIBRARIES}")

    include_directories(${FREETYPE_LIBRARIES})
    target_link_libraries(SnPRecompiled PRIVATE ${FREETYPE_LIBRARIES} SDL2::SDL2 X11)

    set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
    set(THREADS_PREFER_PTHREAD_FLAG TRUE)
    find_package(Threads REQUIRED)

    target_link_libraries(SnPRecompiled PRIVATE "-latomic -static-libstdc++" ${CMAKE_DL_LIBS} Threads::Threads)
endif()

if (ANDROID)
    target_link_libraries(SnPRecompiled PRIVATE SDL2::SDL2 Freetype::Freetype android log)
    target_sources(SnPRecompiled PRIVATE
        ${CMAKE_SOURCE_DIR}/src/android/android_startup.cpp
        ${CMAKE_SOURCE_DIR}/src/android/touch_controls.cpp
        ${CMAKE_SOURCE_DIR}/src/android/descore/touch/descore_touch.c
    )
    # descore's touch module, drawn by the app's Java overlay rather than in GL.
    set_source_files_properties(${CMAKE_SOURCE_DIR}/src/android/descore/touch/descore_touch.c
        PROPERTIES COMPILE_DEFINITIONS DESCORE_TOUCH_NO_GL)
    target_include_directories(SnPRecompiled PRIVATE ${CMAKE_SOURCE_DIR}/src/android)
    # rt64 links SDL2 everywhere except Android, and recompui / recompinput only add the SDL
    # include folders on Windows, macOS and Linux.
    target_link_libraries(rt64 SDL2::SDL2)
    target_include_directories(SnPRecompiled PRIVATE ${SDL2_INCLUDE_DIRS})
endif()

# TODO fix the rt64 CMake script so that this doesn't need to be duplicated here
# For DXC
set (DXC_COMMON_OPTS "-I${PROJECT_SOURCE_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")
set (DXC_CS_OPTS "${DXC_COMMON_OPTS}" "-E" "CSMain" "-T cs_6_0")
set (DXC_GS_OPTS "${DXC_COMMON_OPTS}" "-E" "GSMain" "-T gs_6_0")
set (DXC_RT_OPTS "${DXC_COMMON_OPTS}" "-D" "RT_SHADER" "-T" "lib_6_3" "-fspv-target-env=vulkan1.1spirv1.4" "-fspv-extension=SPV_KHR_ray_tracing" "-fspv-extension=SPV_EXT_descriptor_indexing")

if (${WIN32})
    add_compile_definitions(NOMINMAX)
endif()

# DXC (used by recompui for its shaders) runs on the build machine, so it's picked by the host,
# not the target; the two differ when cross-compiling for Android.
if (CMAKE_HOST_WIN32)
    set (DXC "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc.exe")
elseif (CMAKE_HOST_APPLE)
    # Apple's binary is universal, so it'll work on both x86_64 and arm64
    set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
    if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
        set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
    else()
        set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
    endif()
else()
    if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
        set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc-linux")
    else()
        set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-linux")
    endif()
endif()

set(RECOMP_FRONTEND_N64MODERNRUNTIME_PATH ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
set(RECOMP_FRONTEND_RT64_PATH ${CMAKE_SOURCE_DIR}/lib/rt64)

# Local frontend changes (lib-patches/RecompFrontend/), applied the same way.
snp_apply_lib_patches(RecompFrontend)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RecompFrontend)

# Upstream RmlUi 6.0 uses uint32_t and friends without including <cstdint>, which clang with a
# current libstdc++ rejects, and recompui includes those headers. Add the include from here
# rather than patching the submodule. clang-cl (MSVC frontend variant, incl. Windows) needs the
# native /FI spelling instead of GNU -include: the latter's quote-style header lookup fails to
# resolve bare STL header names (e.g. "cstdint") when clang-cl is generating a precompiled header,
# such as RmlUi's own cmake_pch.cxx, even though INCLUDE-based system search works everywhere else.
if(MSVC)
    set(SNP_CSTDINT_FORCE_INCLUDE "/FI cstdint")
else()
    set(SNP_CSTDINT_FORCE_INCLUDE "-include cstdint")
endif()
if (ANDROID)
    target_include_directories(recompui PRIVATE ${SDL2_INCLUDE_DIRS})
    target_include_directories(recompinput PRIVATE ${SDL2_INCLUDE_DIRS})
endif()

foreach(_snp_rml_target rmlui_core rmlui_debugger recompui)
    if(TARGET ${_snp_rml_target})
        target_compile_options(${_snp_rml_target} PRIVATE "SHELL:$<$<COMPILE_LANGUAGE:CXX>:${SNP_CSTDINT_FORCE_INCLUDE}>")
    endif()
endforeach()

# These libraries reference each other's symbols in both directions (e.g.
# recompinput's player-assignment code calls into recompui's
# ConfigPageControls, RecompiledFuncs calls into librecomp/ultramodern's
# os*_recomp reimplementations) without CMake knowing about the cycle at
# the target level, so a single left-to-right archive scan can leave real,
# resolvable symbols undefined depending on which .o a given archive
# happens to pull in first. --start-group/--end-group makes the linker
# keep re-scanning this whole set until nothing new resolves, same fix as
# for any other circular static-library dependency. This has to be a single
# target_link_libraries call with the group flags as literal arguments --
# CMake collects link *options* (target_link_options) and link
# *libraries* into separate lists and concatenates them at fixed positions
# in the final command line regardless of call order, so two separate
# target_link_options calls do NOT end up wrapping the libraries below.
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT APPLE)
    set(SNP_LINK_GROUP_START "-Wl,--start-group")
    set(SNP_LINK_GROUP_END "-Wl,--end-group")
else()
    set(SNP_LINK_GROUP_START "")
    set(SNP_LINK_GROUP_END "")
endif()

target_link_libraries(SnPRecompiled PRIVATE
    ${SNP_LINK_GROUP_START}
    # PatchesLib must come before RecompiledFuncs. Both define the functions that a RECOMP_PATCH
    # replaces (for example the patched func_80046B20) as weak symbols, and the linker keeps the
    # first definition it finds, so listing RecompiledFuncs first would silently drop the patches.
    PatchesLib
    RecompiledFuncs
    recompui
    recompinput
    librecomp
    ultramodern
    rt64
    nfd
    ${SNP_LINK_GROUP_END}
)


# Embed all .nrm files in the "mods" directory
file(GLOB NRM_FILES "${CMAKE_SOURCE_DIR}/mods/*.nrm")

set(GENERATED_NRM_SOURCES "")

foreach(NRM_FILE ${NRM_FILES})
    get_filename_component(NRM_NAME ${NRM_FILE} NAME_WE)
    set(OUT_C "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.c")
    set(OUT_H "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.h")

    add_custom_command(
        OUTPUT ${OUT_C} ${OUT_H}
        COMMAND file_to_c ${NRM_FILE} ${NRM_NAME} ${OUT_C} ${OUT_H}
        DEPENDS ${NRM_FILE}
    )

    list(APPEND GENERATED_NRM_SOURCES ${OUT_C})
endforeach()

target_sources(SnPRecompiled PRIVATE ${GENERATED_NRM_SOURCES})

# RSP audio microcode (STATUS.md round 1): rsp/n_aspMain.cpp is generated
# from the ROM by RSPRecomp (n_aspMain.jp.toml), like RecompiledFuncs/,
# so it's gitignored and only built when the ROM is present.
if (CMAKE_CROSSCOMPILING)
    # Pre-generated by a desktop build (cmake/CrossCompile.cmake checks it exists) and already
    # in SOURCES; RSPRecomp would be built for the target, so it isn't regenerated here.
    target_compile_definitions(SnPRecompiled PRIVATE SNP_HAS_RSP_AUDIO)
elseif (EXISTS "${SNP_ROM_FILE}")
    list(REMOVE_ITEM SOURCES ${CMAKE_SOURCE_DIR}/rsp/n_aspMain.cpp)
    add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/rsp/n_aspMain.cpp
        COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/rsp
        COMMAND $<TARGET_FILE:RSPRecomp> n_aspMain.jp.toml
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        DEPENDS ${CMAKE_SOURCE_DIR}/n_aspMain.jp.toml RSPRecomp
    )
    target_sources(SnPRecompiled PRIVATE ${CMAKE_SOURCE_DIR}/rsp/n_aspMain.cpp)
    target_compile_definitions(SnPRecompiled PRIVATE SNP_HAS_RSP_AUDIO)
endif()

target_sources(SnPRecompiled PRIVATE ${SOURCES})

set_property(TARGET SnPRecompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")

# Windows crash logs name the host functions on the faulting thread's stack
# (src/main/crash_handler.cpp), from a PDB next to the exe. The recompiled game
# code has no debug info (it would be huge); the crash log names it by N64
# address instead.
if (MSVC)
    foreach(target SnPRecompiled rt64 recompui recompinput librecomp ultramodern)
        if (TARGET ${target})
            target_compile_options(${target} PRIVATE /Z7)
        endif()
    endforeach()
    target_link_options(SnPRecompiled PRIVATE /DEBUG /OPT:REF /OPT:ICF)
    target_link_libraries(SnPRecompiled PRIVATE Dbghelp.lib)
endif()
