# bbport GPU library: shadPS4 video core + shader recompiler + Gnm/VideoOut
# drivers, adapted to the native_probe runtime through shims in shim/.
cmake_minimum_required(VERSION 3.24)
project(bbgpu LANGUAGES C CXX)
# Newer libc++ (clang 21+) no longer includes these transitively; the shadPS4, sirit and FFX
# sources rely on them (std::abort, _countof, swprintf_s, log2, placement new, ...).
if(WIN32)
    foreach(header cstdlib cstring cstdio cstdint cmath cwchar new limits utility algorithm)
        add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:SHELL:-include ${header}>")
    endforeach()
endif()
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()
# Frame pointers keep the loader's thread dumps (watchdog, SIGUSR2 sampling) walkable.
add_compile_options(-fno-omit-frame-pointer)

# bbport: link-time optimization (BB_LTO, default on) and profile-guided optimization
# (BB_PGO: off, generate = instrumented build writing into BB_PGO_DIR, use = build with that
# profile). Covers bbgpu and the static libraries linked into it. No -march: the same build
# runs on the Steam Deck.
option(BB_LTO "Link-time optimization of bbgpu" ON)
# g2-arm64: network/PSN co-op (shadNet: protobuf, OpenSSL) is optional; off on Android.
if (ANDROID)
    option(BB_NET "Network and PSN libraries (co-op)" OFF)
else()
    option(BB_NET "Network and PSN libraries (co-op)" ON)
endif()
set(BB_PGO "off" CACHE STRING "Profile-guided optimization: off, generate, use")
set(BB_PGO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../pgo" CACHE PATH "Profile directory")
if (BB_LTO)
    include(CheckIPOSupported)
    check_ipo_supported(RESULT bb_ipo_ok OUTPUT bb_ipo_error)
    if (bb_ipo_ok)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
    else()
        message(WARNING "LTO unavailable: ${bb_ipo_error}")
    endif()
endif()
if (BB_PGO MATCHES "^(generate|use)$" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    # Functions local to a file (static, lambdas, anonymous namespaces, coroutine parts: a fifth
    # of the profile, command processor included) are named in the profile with their source
    # path. Without this the path holds the checkout's folder, so a profile recorded in one
    # folder missed all of them in any other (another worktree, the GitHub runner). The leading
    # folders up to the repository go: names read "gpu/...;f" or "src/...;f" everywhere.
    get_filename_component(bb_repo_root "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
    string(REGEX MATCHALL "/" bb_repo_seps "${bb_repo_root}/")
    list(LENGTH bb_repo_seps bb_repo_depth)
    add_compile_options("SHELL:-mllvm -static-func-strip-dirname-prefix=${bb_repo_depth}")
endif()
if (BB_PGO STREQUAL "generate" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    # perf7, clang (Windows): IR instrumentation; the profile goes where LLVM_PROFILE_FILE says and is
    # written every 30 s by bbgpu.cpp (the game often ends without the exit-time write). Atomic
    # counters: several threads run the same code.
    add_compile_options(-fprofile-generate -fprofile-update=atomic)
    add_link_options(-fprofile-generate)
    add_compile_definitions(BB_PGO_GENERATE BB_PGO_CLANG)
elseif (BB_PGO STREQUAL "use" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    # perf7, clang: the merged profile (llvm-profdata merge -o pgo/merged.profdata pgo/*.profraw).
    # Functions changed since the profile keep the normal optimization. The IR profile's pass
    # names each function whose code changed ("hash mismatch") or that is new ("no profile
    # data"): build.sh counts them, so the build tells when the profile is due again. Warnings
    # only, never errors.
    add_compile_options(-fprofile-use=${BB_PGO_DIR}/merged.profdata
                        "SHELL:-mllvm -pgo-warn-missing-function"
                        -Wbackend-plugin -Wno-error=backend-plugin
                        -Wno-profile-instr-out-of-date -Wno-profile-instr-unprofiled)
elseif (BB_PGO STREQUAL "generate")
    # Atomic counters: the GPU, recording, preparation and copy threads run the same code.
    add_compile_options(-fprofile-generate=${BB_PGO_DIR} -fprofile-update=atomic)
    add_link_options(-fprofile-generate=${BB_PGO_DIR})
    add_compile_definitions(BB_PGO_GENERATE)
elseif (BB_PGO STREQUAL "use")
    # Partial training keeps unprofiled code optimized normally; counters from concurrent
    # threads can be slightly inconsistent (correction). Functions changed since the profile
    # (or built differently for it, bbgpu_init) are compiled without it.
    add_compile_options(-fprofile-use=${BB_PGO_DIR} -fprofile-partial-training
                        -fprofile-correction -Wno-missing-profile -Wno-coverage-mismatch)
endif()

find_package(Vulkan REQUIRED)
find_package(fmt REQUIRED)
find_package(Boost REQUIRED)
# bbport (Windows): MSYS2 has no magic_enum/miniz packages; the submodules are used instead.
find_package(magic_enum QUIET)
if (NOT magic_enum_FOUND)
    add_library(magic_enum INTERFACE)
    add_library(magic_enum::magic_enum ALIAS magic_enum)
    target_include_directories(magic_enum INTERFACE third_party/magic_enum/include
                                                    third_party/magic_enum/include/magic_enum)
endif()
find_package(tsl-robin-map REQUIRED)
find_package(VulkanMemoryAllocator CONFIG QUIET)
if (NOT VulkanMemoryAllocator_FOUND)
    # MSYS2 ships the header (include/vma/vk_mem_alloc.h) without a CMake package.
    find_path(VMA_INCLUDE_DIR vk_mem_alloc.h PATH_SUFFIXES vma REQUIRED)
    add_library(VulkanMemoryAllocator INTERFACE)
    add_library(GPUOpen::VulkanMemoryAllocator ALIAS VulkanMemoryAllocator)
    target_include_directories(VulkanMemoryAllocator INTERFACE ${VMA_INCLUDE_DIR})
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
if (NOT WIN32 AND NOT ANDROID)
    pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
endif()
find_package(miniz QUIET)
if (NOT miniz_FOUND)
    set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
    set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
    set(BUILD_FUZZERS OFF CACHE BOOL "" FORCE)
    set(BUILD_SHARED_LIBS OFF)
    set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
    add_subdirectory(third_party/miniz EXCLUDE_FROM_ALL)
    unset(CMAKE_POLICY_VERSION_MINIMUM)
    add_library(miniz::miniz ALIAS miniz)
endif()
find_package(Zydis REQUIRED)
find_program(GLSLANG_COMPILER NAMES glslang glslangValidator REQUIRED)

set(SIRIT_USE_SYSTEM_SPIRV_HEADERS OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/sirit EXCLUDE_FROM_ALL)

# bbport: FSR 3.1 temporal upscaling / frame generation, native Vulkan (MIT, FireBurn/FSR-Vulkan).
set(FFX_VK_PORTABLE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FFX_VK_PORTABLE_BUILD_PROBE OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/fsr-vulkan EXCLUDE_FROM_ALL)

# Host shaders -> headers (same scheme as shadPS4's host_shaders/CMakeLists.txt).
set(SHADER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/shadps4/video_core/host_shaders)
set(SHADER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include)
set(SHADER_DIR ${SHADER_INCLUDE}/video_core/host_shaders)
set(SHADER_HEADERS "")
function(add_host_shader FILENAME SHADER_NAME)
    set(OUT ${SHADER_DIR}/${SHADER_NAME}.h)
    string(TOUPPER ${SHADER_NAME} SYMBOL)
    set(DEFINES ${ARGN})
    list(TRANSFORM DEFINES PREPEND "-D")
    add_custom_command(OUTPUT ${OUT}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_DIR}
        COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.3 -o ${OUT} --vn ${SYMBOL} ${DEFINES} ${SHADER_SRC}/${FILENAME}
        COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${SHADER_SRC}/AddShaderHeaderInclude.cmake
        DEPENDS ${SHADER_SRC}/${FILENAME} VERBATIM)
    set(SHADER_HEADERS ${SHADER_HEADERS} ${OUT} PARENT_SCOPE)
endfunction()
add_host_shader(color_to_ms_depth.frag color_to_ms_depth_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_msaa_frag SRC_MSAA=1)
add_host_shader(fault_buffer_process.comp fault_buffer_process_comp)
add_host_shader(fs_tri.vert fs_tri_vert)
add_host_shader(fsr.comp fsr_easu_comp SAMPLE_EASU=1)
add_host_shader(fsr.comp fsr_rcas_comp SAMPLE_RCAS=1)
add_host_shader(post_process.frag post_process_frag)
add_host_shader(present_rotate.frag present_rotate_frag)
add_host_shader(camera_motion.comp camera_motion_comp)
add_host_shader(upscale_merge.comp upscale_merge_comp)
add_host_shader(upscale_reactive.comp upscale_reactive_comp)
add_host_shader(taa.comp taa_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_ldr_comp LDR_OUTPUT=1)
add_host_shader(buffer_multi_copy.comp buffer_multi_copy_comp)
add_host_shader(skin_batch.comp skin_batch_comp)
add_host_shader(skin_mv_batch.comp skin_mv_batch_comp)
add_host_shader(skin_u3_batch.comp skin_u3_batch_comp)
add_host_shader(depth_resample.frag depth_resample_frag)
add_host_shader(depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_host_shader(depth_resample.frag depth_stencil_bits_frag STENCIL_BITS=1)
add_host_shader(camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
add_host_shader(frame_gen_inputs.comp frame_gen_inputs_comp)
add_host_shader(frame_gen_ui.comp frame_gen_ui_comp)
foreach(BPP IN ITEMS 8 16 32 64 96 128)
    add_host_shader(tiling.comp tiling_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
    add_host_shader(tiling.comp tiling_macro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=1)
endforeach()
add_custom_target(host_shaders DEPENDS ${SHADER_HEADERS})

file(GLOB_RECURSE VIDEO_CORE CONFIGURE_DEPENDS shadps4/video_core/*.cpp)
file(GLOB_RECURSE RECOMPILER CONFIGURE_DEPENDS shadps4/shader_recompiler/*.cpp)
list(FILTER VIDEO_CORE EXCLUDE REGEX "renderdoc\\.cpp$")
set(COMMON
    shadps4/common/decoder.cpp shadps4/common/error.cpp
    shadps4/common/io_file.cpp shadps4/common/native_clock.cpp shadps4/common/number_utils.cpp
    shadps4/common/path_util.cpp shadps4/common/rdtsc.cpp shadps4/common/spin_lock.cpp shadps4/common/recursive_lock.cpp
    shadps4/common/string_util.cpp shadps4/common/thread.cpp shadps4/common/signal_context.cpp
    shim/common/logging/log.cpp)
file(GLOB AVPLAYER CONFIGURE_DEPENDS shadps4/core/libraries/avplayer/*.cpp)
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavformat libavcodec libavutil libswscale libswresample)
file(GLOB SHIM_SOURCES CONFIGURE_DEPENDS shim/*.cpp shim/*/*.cpp)
# Windows: a DLL cannot leave the runtime_* symbols for bb-probe.exe to resolve, so the
# library is static there and linked into the executable.
if (WIN32)
    set(BBGPU_KIND STATIC)
else()
    set(BBGPU_KIND SHARED)
endif()
add_library(bbgpu ${BBGPU_KIND} ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES}
    shadps4/core/libraries/gnmdriver/gnmdriver.cpp
    shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
    shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
    shadps4/core/libraries/videodec/video_utils.cpp
    shadps4/core/libraries/videodec/videodec2.cpp shadps4/core/libraries/videodec/videodec2_impl.cpp)
add_dependencies(bbgpu host_shaders)
# bbport co-op: PS4 network and PSN libraries with the shadNet client, from shadp2p
# (github.com/Wozzardman/shadp2p, GPL-2.0), for private Bloodborne co-op servers.
if (BB_NET)
find_package(Protobuf CONFIG QUIET)
if (NOT Protobuf_FOUND)
    # distributions with protobuf 3.x (Debian) ship no CMake package.
    find_package(Protobuf REQUIRED)
endif()
set(SHADNET_PROTO_OUT "${CMAKE_CURRENT_BINARY_DIR}/shadnet_proto_gen")
file(MAKE_DIRECTORY "${SHADNET_PROTO_OUT}")
add_custom_command(
    OUTPUT "${SHADNET_PROTO_OUT}/shadnet.pb.cc" "${SHADNET_PROTO_OUT}/shadnet.pb.h"
    COMMAND protobuf::protoc "--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet"
            "--cpp_out=${SHADNET_PROTO_OUT}" "${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet/shadnet.proto"
    DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet/shadnet.proto")
file(GLOB_RECURSE BBNET_SOURCES CONFIGURE_DEPENDS shadps4/core/libraries/network/*.cpp
    shadps4/core/libraries/np/*.cpp shadps4/shadnet/*.cpp)
# Trophies and the system dialogs stay in the C runtime; their shadPS4 versions need the
# emulator's UI layers.
list(FILTER BBNET_SOURCES EXCLUDE REGEX
    "np_trophy|trophy_ui|np_sns_facebook_dialog|np_profile_dialog/|np_commerce/")
find_package(nlohmann_json CONFIG REQUIRED)
add_library(bbnet STATIC ${BBNET_SOURCES} "${SHADNET_PROTO_OUT}/shadnet.pb.cc" bbnet/bbnet_glue.cpp)
target_include_directories(bbnet PRIVATE shim shadps4 shadps4/shadnet "${SHADNET_PROTO_OUT}"
    third_party/imgui third_party/httplib)
target_link_libraries(bbnet PRIVATE nlohmann_json::nlohmann_json)
# HTTPS (the community game server only speaks TLS) and gzip replies in cpp-httplib.
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
target_compile_definitions(bbnet PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT CPPHTTPLIB_ZLIB_SUPPORT)
target_link_libraries(bbnet PRIVATE OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB)
if (WIN32)
    target_sources(bbnet PRIVATE third_party/wepoll/wepoll.c)
    target_include_directories(bbnet PRIVATE third_party/wepoll)
endif()
    find_path(MINIUPNPC_INCLUDE miniupnpc.h PATH_SUFFIXES miniupnpc REQUIRED)
    find_library(MINIUPNPC_LIBRARY NAMES libminiupnpc.a miniupnpc REQUIRED)
    target_include_directories(bbnet PRIVATE "${MINIUPNPC_INCLUDE}")
if (WIN32)
    target_compile_definitions(bbnet PRIVATE MINIUPNP_STATICLIB)
endif()
    target_link_libraries(bbnet PRIVATE "${MINIUPNPC_LIBRARY}")
target_compile_definitions(bbnet PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbnet PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
target_link_libraries(bbnet PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
    protobuf::libprotobuf)
if (WIN32)
    target_compile_definitions(bbnet PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN)
    target_compile_options(bbnet PRIVATE
        $<$<COMPILE_LANGUAGE:CXX>:-include ${CMAKE_CURRENT_SOURCE_DIR}/shim/bb_win_compat.h>)
    target_link_libraries(bbnet PUBLIC ws2_32 iphlpapi bcrypt crypt32)
endif()
target_link_libraries(bbgpu PUBLIC bbnet)
else()
    # g2-arm64: no co-op; bbnet_register/bbnet_configure are no-ops.
    target_sources(bbgpu PRIVATE bbnet/bbnet_stub.cpp)
endif()
# In-game settings menu: Dear ImGui (third_party/imgui, MIT) with its Vulkan backend, and an
# embedded Cyrillic font (third_party/fonts).
target_sources(bbgpu PRIVATE third_party/imgui/imgui.cpp third_party/imgui/imgui_draw.cpp
    third_party/imgui/imgui_tables.cpp third_party/imgui/imgui_widgets.cpp
    third_party/imgui/backends/imgui_impl_vulkan.cpp)
target_include_directories(bbgpu PRIVATE third_party/imgui third_party/imgui/backends)
target_compile_definitions(bbgpu PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES
    BB_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf")
set_source_files_properties(shim/bbport_overlay.cpp PROPERTIES
    OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf)
if(ANDROID)
    # g2-arm64: the touch menu (a panel on the right edge) instead of the PC window, and its
    # controller setup (issue #16).
    target_sources(bbgpu PRIVATE shim/bbport_overlay_android.cpp shim/bbport_touchpad.cpp
                   shim/bbport_padmap.cpp)
endif()
target_include_directories(bbgpu PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
target_compile_definitions(bbgpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbgpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
if (NOT WIN32)
    target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
endif()
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable ffx-vulkan::fsr4-v07-vulkan
    ffx-vulkan::fsr3-vk-framegeneration-3.1.6)
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
    tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
if (WIN32)
    # Guest code is entered with the System V ABI; NOMINMAX/WIN32_LEAN_AND_MEAN keep
    # <windows.h> out of the way of std::min/max and the Vulkan/SDL headers.
    target_compile_definitions(bbgpu PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN)
    target_compile_options(bbgpu PRIVATE -include ${CMAKE_CURRENT_SOURCE_DIR}/shim/bb_win_compat.h)
    target_include_directories(bbgpu PRIVATE third_party/xbyak)
    target_link_libraries(bbgpu PUBLIC Vulkan::Vulkan ws2_32 bcrypt ntdll synchronization winmm)
elseif (ANDROID)
    target_link_libraries(bbgpu PUBLIC log android)
    # g2-arm64: calls between bbgpu's own functions bind inside the library instead of going
    # through the PLT (default visibility made each one an indirect branch via the GOT: ~3% of
    # the draw recording thread). libmain defines none of them.
    target_link_options(bbgpu PRIVATE -Wl,-Bsymbolic-functions)
    target_include_directories(bbgpu PRIVATE third_party/xbyak) # SRT walker assembler
    target_compile_definitions(bbgpu PRIVATE XBYAK64_GCC)      # x86-64 code on an ARM64 host
    # g2-arm64: libadrenotools (BSD-2) loads a custom Vulkan driver (Turnip) through the system
    # loader in a separate linker namespace (BB_VK_DRIVER, vk_platform.cpp).
    set(ADRENOTOOLS_SOURCE_DIR "$ENV{HOME}/src/libadrenotools" CACHE PATH "libadrenotools sources")
    set(ADRENOTOOLS_BUILD_DIR "$ENV{HOME}/src/adrenotools-android" CACHE PATH "libadrenotools build")
    if (EXISTS "${ADRENOTOOLS_BUILD_DIR}/libadrenotools.a")
        target_include_directories(bbgpu PRIVATE "${ADRENOTOOLS_SOURCE_DIR}/include")
        target_compile_definitions(bbgpu PRIVATE BB_ADRENOTOOLS)
        target_link_libraries(bbgpu PRIVATE "${ADRENOTOOLS_BUILD_DIR}/libadrenotools.a"
            "${ADRENOTOOLS_BUILD_DIR}/lib/linkernsbypass/liblinkernsbypass.a")
    else()
        message(WARNING "libadrenotools not found (ADRENOTOOLS_BUILD_DIR=${ADRENOTOOLS_BUILD_DIR}): "
            "the app can use only the system Vulkan driver, not Turnip (BB_VK_DRIVER is ignored)")
    endif()
else()
    # the xbyak submodule, as on Windows (no system package needed).
    target_include_directories(bbgpu PRIVATE third_party/xbyak)
    target_link_libraries(bbgpu PUBLIC PkgConfig::X11)
endif()

# bbport (Windows): bbgpu is a static library there, so the loader and the HLE runtime are
# linked here, where CMake knows its dependencies (build.sh builds them with ninja).
if (WIN32)
    enable_language(RC)
    file(GLOB BB_RUNTIME CONFIGURE_DEPENDS ../src/runtime*.c)
    file(GLOB BB_ATRAC9 CONFIGURE_DEPENDS ../third_party/LibAtrac9/C/src/*.c)
    add_library(atrac9 STATIC ${BB_ATRAC9})
    target_compile_options(atrac9 PRIVATE -w)
    set_target_properties(atrac9 PROPERTIES C_STANDARD 99 INTERPROCEDURAL_OPTIMIZATION OFF)
    add_executable(bb-probe ../src/probe.c ../src/red_zone.c ../src/cpu_patch.c ${BB_RUNTIME} ../src/compat_win.c ../src/vulkan_smoke.c
        ../src/guest_cpu_native.c ../packaging/windows/bb-probe.rc)
    set_target_properties(bb-probe PROPERTIES C_STANDARD 11 C_EXTENSIONS OFF)
    target_compile_options(bb-probe PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wall -Wextra -Werror>)
    target_include_directories(bb-probe PRIVATE .. ../src)
    target_link_libraries(bb-probe PRIVATE bbgpu atrac9 Zydis::Zydis onecore bcrypt ws2_32 -pthread)
    # The PS4 range (64 GiB - 1 TiB) must stay free: no high-entropy ASLR for heaps and stacks.
    # The guest main thread runs on the process stack: 16 MiB, committed (no stack probes).
    target_link_options(bb-probe PRIVATE -Wl,--disable-high-entropy-va "SHELL:-Xlinker --stack -Xlinker 16777216,16777216")
    add_executable(bb-gpu-capabilities ../tools/gpu_capabilities.c ../packaging/windows/bb-gpu-capabilities.rc)
    target_link_libraries(bb-gpu-capabilities PRIVATE Vulkan::Vulkan)
    # "Play Bloodborne.exe": the game with the launcher's saved settings, no launcher window.
    add_executable(bb-play WIN32 ../packaging/windows/play.c ../packaging/windows/play.rc)
    target_compile_options(bb-play PRIVATE -municode)
    target_link_options(bb-play PRIVATE -municode)
endif()

# g2-arm64: the loader for Android arm64 (guest code on an embedded FEXCore, guest_cpu_fex.cpp).
# FEX_SOURCE_DIR / FEX_BUILD_DIR: a FEXCore build for Android (uncharted-g2 scripts/port-arm64).
if (ANDROID)
    set(FEX_SOURCE_DIR "$ENV{HOME}/src/fex" CACHE PATH "FEX sources")
    set(FEX_BUILD_DIR "$ENV{HOME}/src/fex-android" CACHE PATH "FEXCore build for Android")
    file(GLOB BB_RUNTIME CONFIGURE_DEPENDS ../src/runtime*.c)
    file(GLOB BB_ATRAC9 CONFIGURE_DEPENDS ../third_party/LibAtrac9/C/src/*.c)
    add_library(atrac9 STATIC ${BB_ATRAC9})
    target_compile_options(atrac9 PRIVATE -w)
    set_target_properties(atrac9 PROPERTIES C_STANDARD 99)
    # bb-probe: a command-line loader for adb shell (--cpu-only); main: the same as libmain.so,
    # whose SDL_main the APK's SDLActivity calls (uncharted-g2 scripts/g2/game_app).
    add_executable(bb-probe ../src/probe.c ${BB_RUNTIME} ../src/guest_cpu_fex.cpp ../src/vulkan_smoke.c)
    add_library(main SHARED ../src/probe.c ${BB_RUNTIME} ../src/guest_cpu_fex.cpp ../src/vulkan_smoke.c)
    target_compile_definitions(main PRIVATE BB_SDL_MAIN)
    target_link_options(bb-probe PRIVATE -Wl,--export-dynamic) # runtime_*/guest_call for libbbgpu.so
    foreach(loader bb-probe main)
    set_target_properties(${loader} PROPERTIES C_STANDARD 11 C_EXTENSIONS ON CXX_STANDARD 20)
    target_compile_options(${loader} PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wall>)
    target_include_directories(${loader} PRIVATE .. ../src)
    target_include_directories(${loader} PRIVATE
        ${FEX_BUILD_DIR} ${FEX_SOURCE_DIR} ${FEX_SOURCE_DIR}/Source ${FEX_BUILD_DIR}/Source
        ${FEX_BUILD_DIR}/generated ${FEX_SOURCE_DIR}/Source/Tools/CommonTools ${FEX_BUILD_DIR}/FEXCore/Source
        ${FEX_SOURCE_DIR}/FEXCore/include ${FEX_BUILD_DIR}/include
        ${FEX_SOURCE_DIR}/External/unordered_dense/include ${FEX_SOURCE_DIR}/External/fmt/include
        ${FEX_SOURCE_DIR}/External/xxhash ${FEX_SOURCE_DIR}/FEXHeaderUtils ${FEX_SOURCE_DIR}/CodeEmitter
        ${FEX_SOURCE_DIR}/External/range-v3/include ${FEX_SOURCE_DIR}/External/tiny-json)
    endforeach()
    set_source_files_properties(../src/guest_cpu_fex.cpp PROPERTIES COMPILE_DEFINITIONS
        "ARCHITECTURE_arm64=1;FEXCORE_HAS_PRESERVE_ALL_ATTR=1;FEXCORE_PRESERVE_ALL_ATTR=__attribute__((preserve_all));FEX_HAS_PRESERVE_ALL_ATTR=1;FEX_PRESERVE_ALL_ATTR=__attribute__((preserve_all));FEX_DISABLE_TELEMETRY=1")
    set(FEX_LIBS FEXCore/Source/libFEXCore.a Source/Common/libCommon.a Source/Tools/CommonTools/libCommonTools.a
        FEXCore/Source/libJemallocLibs.a Source/Common/cpp-optparse/libcpp-optparse.a External/tiny-json/libtiny-json.a
        FEXCore/Source/libFEXCore_Base.a External/fmt/libfmt.a External/xxhash/cmake_unofficial/libxxhash.a
        External/cephes/libcephes_128bit.a External/SoftFloat-3e/libsoftfloat_3e.a External/rpmalloc/librpmalloc.a)
    list(TRANSFORM FEX_LIBS PREPEND "${FEX_BUILD_DIR}/")
    foreach(loader bb-probe main)
        target_link_libraries(${loader} PRIVATE bbgpu atrac9 -Wl,--start-group ${FEX_LIBS} -Wl,--end-group
            Vulkan::Vulkan dl atomic m log)
    endforeach()
    # runtime_restart: SDL's JNI environment and activity (the app restarts itself)
    target_link_libraries(main PRIVATE PkgConfig::SDL3)
    # The app's launcher: the game files from the user's dump (scripts/prepare.py and
    # link_modules.py in C), without loading the game's libraries.
    add_library(bbprepare SHARED ../src/game_prepare.c ../src/game_prepare_jni.c)
    set_target_properties(bbprepare PROPERTIES C_STANDARD 11)
    target_compile_options(bbprepare PRIVATE -Wall -Wextra)
endif()

# Explicit test targets; neither launches the game nor needs a Vulkan device.
add_executable(motion-history-test EXCLUDE_FROM_ALL ../tests/test_motion_history.cpp)
target_compile_options(motion-history-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(motion-history-test PRIVATE ..)
add_executable(motion-shader-test EXCLUDE_FROM_ALL ../tests/test_motion_shaders.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(motion-shader-test PRIVATE shim shadps4 third_party/gcn/include)
target_link_libraries(motion-shader-test PRIVATE bbgpu)
add_executable(ui-composition-test EXCLUDE_FROM_ALL ../tests/test_ui_composition.cpp)
target_compile_options(ui-composition-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(ui-composition-test PRIVATE ..)

add_executable(scene-resolution-test EXCLUDE_FROM_ALL ../tests/test_scene_resolution.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(scene-resolution-test PRIVATE shim shadps4 third_party/gcn/include)
# The test's Vulkan-Hpp inline methods have a different set of platform extensions
# from vk_platform.cpp. Keep them local and use an explicit dispatcher in the test.
target_compile_options(scene-resolution-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_scene_resolution.cpp PROPERTIES
    COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_link_libraries(scene-resolution-test PRIVATE bbgpu)
add_executable(taa-shader-test EXCLUDE_FROM_ALL ../tests/test_taa_shader.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(taa-shader-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(taa-shader-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_taa_shader.cpp PROPERTIES
    COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_link_libraries(taa-shader-test PRIVATE bbgpu)
add_dependencies(taa-shader-test host_shaders)

add_executable(camera-motion-test EXCLUDE_FROM_ALL ../tests/test_camera_motion.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(camera-motion-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(camera-motion-test PRIVATE -UNDEBUG -fPIC)
target_link_libraries(camera-motion-test PRIVATE bbgpu)
add_dependencies(camera-motion-test host_shaders)

# bbport: FSR 4 benchmark outside the game (tools/fsr4_bench.cpp): ninja -C out/gpu fsr4-bench.
add_executable(upscaler-support-test EXCLUDE_FROM_ALL ../tests/test_upscaler_support.cpp
    shim/bbport_settings.cpp)
target_include_directories(upscaler-support-test PRIVATE shim)
target_compile_options(upscaler-support-test PRIVATE -UNDEBUG)

add_executable(fsr4-bench EXCLUDE_FROM_ALL tools/fsr4_bench.cpp
    shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp)
target_include_directories(fsr4-bench PRIVATE shadps4/video_core/renderer_vulkan/fsr411)
target_link_libraries(fsr4-bench PRIVATE ffx-vulkan::fsr4-v07-vulkan ffx-vulkan::fsr4-v07-assets Vulkan::Vulkan)
