cmake_minimum_required(VERSION 3.20)
project(ResistanceRecomp
    VERSION 0.1.0
    LANGUAGES C CXX
    DESCRIPTION "Recompiled PS3 game project (ps3recomp)"
)

# ---------------------------------------------------------------------------
# Standards
# ---------------------------------------------------------------------------
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Prefer Ninja if available
if(NOT CMAKE_GENERATOR MATCHES "Ninja" AND NOT DEFINED ENV{CMAKE_GENERATOR})
    message(STATUS "Tip: use -G Ninja for faster builds")
endif()

# ---------------------------------------------------------------------------
# The two directories this project is built from
# ---------------------------------------------------------------------------
# PS3RECOMP_DIR is the toolkit checkout: include/, runtime/, libs/, tools/.
# RECOMP_DIR is where the lifter's output for THIS game lives. They are
# separate on purpose -- a port keeps its generated code in its own tree and
# points at whatever toolkit checkout it is building against:
#
#   cmake -B build -G Ninja \
#         -DPS3RECOMP_DIR=/path/to/ps3recomp \
#         -DRECOMP_DIR=/path/to/my_game/recompiled
#
if(NOT DEFINED PS3RECOMP_DIR)
    # Default: this project is still sitting inside ps3recomp/templates/project/
    get_filename_component(PS3RECOMP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../" ABSOLUTE)
endif()
get_filename_component(PS3RECOMP_DIR "${PS3RECOMP_DIR}" ABSOLUTE)

set(RECOMP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/recompiled" CACHE PATH
    "Directory holding the lifter's output: ppu_recomp.h and the lifted sources")
get_filename_component(RECOMP_DIR "${RECOMP_DIR}" ABSOLUTE)

message(STATUS "ps3recomp SDK:   ${PS3RECOMP_DIR}")
message(STATUS "Lifted output:   ${RECOMP_DIR}")

if(NOT EXISTS "${RECOMP_DIR}/ppu_recomp.h")
    message(FATAL_ERROR
        "No ppu_recomp.h in ${RECOMP_DIR}.\n"
        "That header and the lifted sources beside it are what "
        "tools/ppu_lifter.py writes. Run the lifter, drop its output there, "
        "or point -DRECOMP_DIR= at wherever it already is.")
endif()

# The lifted translation units. A game's lift is hundreds of generated files,
# so they are globbed rather than listed; re-run CMake after a fresh lift.
file(GLOB RECOMP_SOURCES CONFIGURE_DEPENDS
    "${RECOMP_DIR}/*.c"
    "${RECOMP_DIR}/*.cpp"
)
if(NOT RECOMP_SOURCES)
    message(FATAL_ERROR
        "${RECOMP_DIR} has a ppu_recomp.h but no .c or .cpp beside it. The "
        "lifted functions and the function table live in those; without them "
        "there is nothing to run.")
endif()
list(LENGTH RECOMP_SOURCES RECOMP_SOURCE_COUNT)
message(STATUS "Lifted sources:  ${RECOMP_SOURCE_COUNT} file(s)")

# Resistance 2 needed a hook in its lifted frontend (port 0 enabled before
# the first local player exists), applied here after each lift. Resistance 3
# starts without one; a lifted-code hook of its own would go here the same way
# (tools/apply_r3_*.py run on RECOMP_DIR at configure time).

# ---------------------------------------------------------------------------
# ps3recomp runtime library
# ---------------------------------------------------------------------------
# Built from source as a subdirectory rather than imported as a prebuilt
# archive. A static library carries none of its own link requirements, so an
# imported one arrives without SDL2, pthreads, the Metal frameworks or the
# Winsock/XInput imports -- the link then fails on every platform for a
# different reason. add_subdirectory hands all of that over with the target.
add_subdirectory("${PS3RECOMP_DIR}" "${CMAKE_BINARY_DIR}/ps3recomp_sdk"
                 EXCLUDE_FROM_ALL)
# The Metal engine's record stream: Resistance 3 filled the default 8192
# records 64+ times a run, each a synchronous mid-frame submit.
target_compile_definitions(ps3recomp_runtime PRIVATE ENG_MAX_RECORDS=16384)

# ---------------------------------------------------------------------------
# The HLE NID table
# ---------------------------------------------------------------------------
# Maps each firmware import NID onto the handler in the runtime that answers
# it. A port may keep a hand-curated one beside its lift; otherwise generate
# the canonical every-module table, which is what the toolkit's own boot
# harness uses.
if(EXISTS "${RECOMP_DIR}/ppu_hle_nids.cpp")
    set(HLE_NID_TABLE "${RECOMP_DIR}/ppu_hle_nids.cpp")
    message(STATUS "HLE NID table:   ${HLE_NID_TABLE}")
else()
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    set(HLE_NID_TABLE "${CMAKE_CURRENT_BINARY_DIR}/ppu_hle_nids.cpp")
    # The generator discovers handlers from library definitions. Rebuild when
    # an existing module changes, and reconfigure when a new module is added.
    file(GLOB_RECURSE HLE_NID_SOURCES CONFIGURE_DEPENDS
        "${PS3RECOMP_DIR}/libs/*.c")
    # A removed input cannot make the output older than a remaining dependency.
    # Track the list itself so deletions also remove stale handler references.
    set(HLE_NID_SOURCE_LIST "${CMAKE_CURRENT_BINARY_DIR}/hle_nid_sources.txt")
    file(GENERATE OUTPUT "${HLE_NID_SOURCE_LIST}" CONTENT "${HLE_NID_SOURCES}\n")
    add_custom_command(
        OUTPUT "${HLE_NID_TABLE}"
        COMMAND "${Python3_EXECUTABLE}"
                "${PS3RECOMP_DIR}/tools/gen_hle_nids.py"
                --out "${HLE_NID_TABLE}" --all
        DEPENDS "${PS3RECOMP_DIR}/tools/gen_hle_nids.py"
                "${PS3RECOMP_DIR}/tools/nid_database.py"
                ${HLE_NID_SOURCES} "${HLE_NID_SOURCE_LIST}"
        COMMENT "Generating the HLE NID -> handler table")
    message(STATUS "HLE NID table:   generated (every module)")
endif()

# ---------------------------------------------------------------------------
# The game executable
# ---------------------------------------------------------------------------
# The PPU boot scaffold is compiled here rather than taken from the runtime
# library: every one of these files #includes the lifter's per-game
# ppu_recomp.h, so they can only be built against a particular lift.
set(PS3RECOMP_SCAFFOLD
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_loader.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_hle.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_fs.cpp"
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_sysprx.cpp"
    # Host side of the lifter's --sjlj (a title's setjmp/longjmp); unused
    # without it.
    "${PS3RECOMP_DIR}/runtime/ppu/ppu_sjlj.c"
)

# ---------------------------------------------------------------------------
# Lifted SPU images (optional)
# ---------------------------------------------------------------------------
# A title that runs SPU work gets its images lifted into <SPU_DIR>/<image>/ by
# tools/spu_lifter.py, with a registration TU generated by
# tools/build_spu_workloads.py. cellSpurs dispatches them by content
# fingerprint, so the generated table is keyed to the images THIS build ships:
# a table generated from another release registers signatures that can never
# match, which is worse than shipping none. Both the table and the job-module
# registry self-register from constructors and are therefore optional -- a tree
# that has not lifted its SPU images yet configures and links without them.
set(SPU_DIR "${CMAKE_CURRENT_SOURCE_DIR}/spu" CACHE PATH
    "Directory holding the lifted SPU images (<SPU_DIR>/<image>/spu_recomp.c)")
option(PORT_ENABLE_SPU "Compile and register the title's lifted SPU images" ON)

set(PORT_SPU_INPUTS "" CACHE STRING
    "Input files to scan for all embedded SPU ELF programs (semicolon-separated decrypted ELF/PRX/data paths)")
set(SPU_SOURCES "")
if(PORT_ENABLE_SPU AND PORT_SPU_INPUTS)
    include("${PS3RECOMP_DIR}/cmake/PS3RecompSPU.cmake")
    # Resistance 3 (BCUS98176 01.00) embeds the Edge zlib inflate task (the
    # .psarc decompressor) as a stripped SPU ELF: no section headers, so its
    # one executable PT_LOAD (LS 0x3000-0xC350) is lifted whole, and from
    # 0xB290 on it is read-only data (the "EDGE ASSERTION FAILURE" strings and
    # inflate tables) the lifter decodes as 213 invalid words. The last
    # function returns at 0xB28C. Named by content hash, so it never applies to
    # another EBOOT's image (the generator skips names it does not find).
    set(PS3RECOMP_SPU_GENERATOR_ARGS
        --text-end spu_d08b1ad818d026818f1c7cbb2bef448d242a9ae51aab4c7bce69f2ad14308355=0xB290)
    ps3recomp_generate_spu("${CMAKE_CURRENT_BINARY_DIR}/generated_spu" ${PORT_SPU_INPUTS})
    list(APPEND SPU_SOURCES ${SPU_GENERATED_SOURCES}
         "${PS3RECOMP_DIR}/runtime/spu/spu_dispatch_mt.c")
elseif(PORT_ENABLE_SPU)
    file(GLOB SPU_LIFTED CONFIGURE_DEPENDS "${SPU_DIR}/*/spu_recomp.c")
    if(SPU_LIFTED)
        list(LENGTH SPU_LIFTED SPU_IMAGE_COUNT)
        message(STATUS "Lifted SPU:      ${SPU_IMAGE_COUNT} image(s)")
        # The musttail indirect-branch fast path. Excluded from the runtime
        # library on purpose (see the SDK root CMakeLists): it must be built
        # here, by clang, or the tail call degrades to a plain call and a guest
        # loop that branches indirectly walks the thread stack off its guard
        # page a few thousand iterations in.
        list(APPEND SPU_SOURCES ${SPU_LIFTED}
             "${PS3RECOMP_DIR}/runtime/spu/spu_dispatch_mt.c")
        # Generated beside the lift; each is present only once its generator
        # has been run, so each is included only if it is actually here.
        foreach(gen IN ITEMS spu_workloads.c jobmods_register.c taskset_policy_bytes.c)
            if(EXISTS "${SPU_DIR}/${gen}")
                list(APPEND SPU_SOURCES "${SPU_DIR}/${gen}")
            else()
                message(STATUS "  (no ${gen} -- not registered)")
            endif()
        endforeach()
    else()
        message(STATUS "Lifted SPU:      none found in ${SPU_DIR}")
    endif()
endif()

# This repository's tools/ and the decrypted EBOOT the port's raw SPU code is
# cut from (tools/rebuild.sh puts it at game/<title>/EBOOT.elf).
set(RES_TITLE_ID "BCUS98176" CACHE STRING "The disc's title ID (BCUS98176 USA, BCES01118 Europe)")
set(R3_TOOLS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../tools")
set(R3_EBOOT "${CMAKE_CURRENT_SOURCE_DIR}/../game/${RES_TITLE_ID}/EBOOT.elf" CACHE FILEPATH
    "The decrypted Resistance 3 EBOOT.elf")

# Raw SPU code the ELF scanner cannot see (in Resistance 2: the Wws_Job policy
# module, the Bink decoder and two level programs, all raw bytes in the EBOOT).
# Each tools/prepare_r3_<name>.py lifts one into spu/r3_<name>/ with a
# register.c beside it, and is run again here with --check, so a lift made
# from a different EBOOT fails the configure instead of registering
# fingerprints that never match.
file(GLOB R3_RAW_LIFTS CONFIGURE_DEPENDS "${SPU_DIR}/r3_*/spu_recomp.c")
if(PORT_ENABLE_SPU AND R3_RAW_LIFTS)
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    foreach(src IN LISTS R3_RAW_LIFTS)
        get_filename_component(dir "${src}" DIRECTORY)
        get_filename_component(name "${dir}" NAME)          # r3_<name>
        string(REGEX REPLACE "^r3_" "" short "${name}")
        if(NOT EXISTS "${dir}/register.c")
            message(STATUS "  (${name}: no register.c -- not registered)")
            continue()
        endif()
        set(tool "${R3_TOOLS_DIR}/prepare_r3_${short}.py")
        if(EXISTS "${tool}")
            set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${tool}" "${R3_EBOOT}")
            execute_process(COMMAND "${Python3_EXECUTABLE}" "${tool}" --check --eboot "${R3_EBOOT}"
                RESULT_VARIABLE rc OUTPUT_VARIABLE msg ERROR_VARIABLE err)
            if(NOT rc EQUAL 0)
                message(FATAL_ERROR "${name}: ${err}")
            endif()
            message(STATUS "${msg}")
        endif()
        list(APPEND SPU_SOURCES "${src}" "${dir}/register.c")
    endforeach()
endif()

# The WWS job modules (0xC0DEC0DE headers in the EBOOT) the Wws_Job policy
# module streams into its job code buffer; interpreted, Resistance 2's first
# level ran ~5 FPS. tools/regen_r3_jobmods.sh lifts them; each registers by
# content signature.
file(GLOB R3_JOBMODS CONFIGURE_DEPENDS "${SPU_DIR}/jobmods/*/spu_recomp.c")
if(PORT_ENABLE_SPU AND R3_JOBMODS AND EXISTS "${SPU_DIR}/jobmods/jobmods_register.c")
    list(LENGTH R3_JOBMODS R3_JOBMOD_COUNT)
    message(STATUS "R3 WWS job modules: ${R3_JOBMOD_COUNT}")
    list(APPEND SPU_SOURCES ${R3_JOBMODS} "${SPU_DIR}/jobmods/jobmods_register.c")
endif()

# Position-independent SPU code the jobs load at any LS address (DEADDEAD
# headers; Resistance 2 had 142). A tools/lift_r3_relocmods.py modelled on
# the Resistance 2 port's writes spu/relocmods/; PS3_RELOCMODS=0 leaves them to
# the interpreter.
file(GLOB R3_RELOCMODS CONFIGURE_DEPENDS "${SPU_DIR}/relocmods/*/spu_recomp.c")
if(PORT_ENABLE_SPU AND R3_RELOCMODS AND EXISTS "${SPU_DIR}/relocmods/relocmods_register.c")
    list(LENGTH R3_RELOCMODS R3_RELOCMOD_COUNT)
    message(STATUS "R3 relocatable SPU modules: ${R3_RELOCMOD_COUNT}")
    list(APPEND SPU_SOURCES ${R3_RELOCMODS} "${SPU_DIR}/relocmods/relocmods_register.c")
endif()

# Insomniac's ig job modules (raw SPU code igPm's job loop copies to LS 0x100:
# igBink and 28 more). tools/lift_r3_igjobs.py lifts them into spu/igjobs/ and
# lists the sources it registered in igjobs_sources.cmake; PS3_IGJOBS=0 leaves
# them to the interpreter.
if(PORT_ENABLE_SPU AND EXISTS "${SPU_DIR}/igjobs/igjobs_sources.cmake")
    find_package(Python3 COMPONENTS Interpreter REQUIRED)
    set(tool "${R3_TOOLS_DIR}/lift_r3_igjobs.py")
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
        "${tool}" "${R3_EBOOT}" "${SPU_DIR}/igjobs/igjobs_sources.cmake")
    execute_process(COMMAND "${Python3_EXECUTABLE}" "${tool}" --check --eboot "${R3_EBOOT}"
        RESULT_VARIABLE rc OUTPUT_VARIABLE msg ERROR_VARIABLE err)
    if(NOT rc EQUAL 0)
        message(FATAL_ERROR "igjobs: ${err}")
    endif()
    message(STATUS "${msg}")
    include("${SPU_DIR}/igjobs/igjobs_sources.cmake")
    list(APPEND SPU_SOURCES ${R3_IGJOB_SOURCES})
endif()

add_executable(${PROJECT_NAME}
    main.cpp
    stubs.cpp
    patches.cpp
    ${PS3RECOMP_SCAFFOLD}
    ${HLE_NID_TABLE}
    ${RECOMP_SOURCES}
    ${SPU_SOURCES}
)

# port/patches/eboot_<version>.words.h (tools/r3_survey.py --write-insn) are
# pulled into patches.cpp with __has_include, which a compiler depfile cannot
# track while they do not exist. The glob re-runs CMake when one appears or
# goes; OBJECT_DEPENDS rebuilds patches.cpp when one changes, and the list in
# a definition changes its command line when the set changes.
file(GLOB R3_PATCH_WORDS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/patches/*.words.h")
set(R3_PATCH_WORD_NAMES "")
foreach(f IN LISTS R3_PATCH_WORDS)
    get_filename_component(n "${f}" NAME_WE)
    string(APPEND R3_PATCH_WORD_NAMES "${n} ")
endforeach()
string(STRIP "${R3_PATCH_WORD_NAMES}" R3_PATCH_WORD_NAMES)
set_source_files_properties(patches.cpp PROPERTIES
    OBJECT_DEPENDS "${R3_PATCH_WORDS}"
    COMPILE_DEFINITIONS "R3_PATCH_WORDS=\"${R3_PATCH_WORD_NAMES}\"")
message(STATUS "Patch words:     ${R3_PATCH_WORD_NAMES}")

# The lifted SPU sources use bare #include "spu_context.h". Scope that include
# directory (and their flags) to those TUs alone: adding it to the whole target
# changes the compile command of every lifted PPU TU, and those are large enough
# that the needless rebuild is measured in tens of minutes.
#   SPU_USE_DISPATCH_MT -- route SPU_IB_DISPATCH through the musttail path.
#   -O2                 -- the target-wide -O1 exists for the huge PPU TUs; the
#                          SPU sources are small and decode-hot, so they get
#                          full optimization. The last -O on the line wins.
if(SPU_SOURCES)
    set_source_files_properties(${SPU_SOURCES} PROPERTIES
        INCLUDE_DIRECTORIES "${PS3RECOMP_DIR}/runtime/spu"
        COMPILE_DEFINITIONS SPU_USE_DISPATCH_MT)
    if(NOT MSVC OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
        set_source_files_properties(${SPU_SOURCES} PROPERTIES COMPILE_OPTIONS "-O2")
    endif()
endif()

# app.rc carries the icon, the version block and the manifest, and needs a
# resource compiler. Listing it as a source on a host that has none is not a
# link error, it is a "no rule to make" for a file nothing can consume.
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/app.rc")
    target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/app.rc")
endif()

target_include_directories(${PROJECT_NAME} PRIVATE
    "${RECOMP_DIR}"                            # the generated ppu_recomp.h
    "${PS3RECOMP_DIR}/include"
    "${PS3RECOMP_DIR}/runtime/ppu"
    "${PS3RECOMP_DIR}/runtime/spu"
    "${PS3RECOMP_DIR}/runtime/memory"
    "${PS3RECOMP_DIR}/runtime/syscalls"
    "${PS3RECOMP_DIR}/runtime/platform"        # win32_compat.h and friends
    "${PS3RECOMP_DIR}/libs/video"
    "${CMAKE_CURRENT_SOURCE_DIR}"
)

target_link_libraries(${PROJECT_NAME} PRIVATE
    ps3recomp_runtime
)

# The runtime is C++, and on Apple also Objective-C. Letting CMake infer the
# link language from the generated C would leave the C++ standard library out.
set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX)

# Windows system import libraries. dbghelp symbolizes crash-handler addresses
# against the PDB, user32 and gdi32 are what the backend's window and its GDI
# present are made of, and winmm is timeBeginPeriod. None of the four names
# anything a POSIX linker can find; the D3D12 backend's own imports come in
# through the #pragma comment(lib) in libs/video/rsx_d3d12_backend.c.
if(WIN32)
    target_link_libraries(${PROJECT_NAME} PRIVATE
        dbghelp
        user32
        gdi32
        winmm
    )
endif()

# ---------------------------------------------------------------------------
# Compiler-specific flags
# ---------------------------------------------------------------------------
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
    target_compile_options(${PROJECT_NAME} PRIVATE
        -Wall -Wextra -Wno-unused-parameter
        -fno-strict-aliasing
        # The lift spells PPC arithmetic as signed C arithmetic, which must wrap;
        # without this, clang exploits overflow UB (e.g. folds "x*x < 0" to false).
        -fwrapv
        # The target-wide -O1 the SPU note above relies on. Without it a build
        # configured with no CMAKE_BUILD_TYPE compiled every lifted PPU TU at
        # -O0: each static inline helper (ppc_rlw64, vmx_w, vm_read32_inl) is
        # a real call that spills to the stack, and those calls were most of
        # the PPU thread's profile. -O1 keeps the huge TUs' build time sane.
        $<$<NOT:$<CONFIG:Debug>>:-O1>
    )
elseif(MSVC)
    target_compile_options(${PROJECT_NAME} PRIVATE
        /W3 /wd4100 /wd4244 /wd4267
        /Zc:__cplusplus         # report correct __cplusplus value
        # ppu_recomp.h defines PPU_FENCE, the lowering of the guest's
        # sync/lwsync/eieio/isync, and in C that is <stdatomic.h>. The lifter
        # writes C++ by default, but --single-file writes ppu_recomp.c, and a
        # port that took it would not compile without this. The runtime's own
        # build already carries the same flag for the same reason.
        /experimental:c11atomics
    )
    target_compile_definitions(${PROJECT_NAME} PRIVATE
        _CRT_SECURE_NO_WARNINGS
    )
endif()

# The lifted translation units are machine-generated and can run to hundreds of
# thousands of lines each, so they get their own flags:
#
#   - warnings off. They are about generated code and nobody will act on them,
#     and at this volume they bury the diagnostics that matter.
#   - no floating-point contraction. arm64 fuses a multiply and an add by
#     default; a PPC fmadd is spelt as one where the lifter means one.
#   - /bigobj on MSVC. A single lifted file routinely passes the 65,536-section
#     limit of the default object format.
if(MSVC)
    set_source_files_properties(${RECOMP_SOURCES} PROPERTIES
        COMPILE_OPTIONS "/w;/bigobj")
else()
    set_source_files_properties(${RECOMP_SOURCES} PROPERTIES
        COMPILE_OPTIONS "-w;-ffp-contract=off")
endif()

# ---------------------------------------------------------------------------
# The window (macOS)
# ---------------------------------------------------------------------------
# A window goes away with the process image that opened it, and an exitspawn
# (Resistance: Fall of Man's game.self) replaces that image. `Resistance` holds
# the window for the whole session instead: it runs ResistanceRecomp with
# PS3RECOMP_HOST_SURFACES set, the Metal backend renders into IOSurfaces it
# shares (libs/video/rsx_host_surface.h), and the image an exec brings up
# attaches to the same window again. One Objective-C file and no runtime
# library, so it builds in seconds. tools/play.sh starts the game through it:
#
#   build/BCUS98176/Resistance build/BCUS98176/ResistanceRecomp game/BCUS98176/EBOOT.elf
if(APPLE)
    enable_language(OBJC)
    add_executable(Resistance "${PS3RECOMP_DIR}/tools/host_window/host_window.m")
    target_include_directories(Resistance PRIVATE "${PS3RECOMP_DIR}/libs/video")
    target_compile_definitions(Resistance PRIVATE HOST_WINDOW_TITLE="Resistance 3")
    target_compile_options(Resistance PRIVATE -fobjc-arc -Wall -Wextra -Wno-unused-parameter)
    target_link_libraries(Resistance PRIVATE
        "-framework AppKit" "-framework Metal" "-framework QuartzCore" "-framework IOSurface")
endif()

# ---------------------------------------------------------------------------
# Install
# ---------------------------------------------------------------------------
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
if(APPLE)
    install(TARGETS Resistance RUNTIME DESTINATION bin)
endif()
