project(LostOdysseyRecomp LANGUAGES CXX)

option(LO_RENDERER_P2_SELFTEST "Build the guarded scene-copy promotion bootstrap entry" OFF)
if(LO_RENDERER_P2_SELFTEST AND NOT LO_BUILD_GPU)
    message(FATAL_ERROR "LO_RENDERER_P2_SELFTEST requires LO_BUILD_GPU")
endif()

set(LO_RUNTIME_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
include("${CMAKE_SOURCE_DIR}/cmake/portable_shader_pack.cmake")
add_library(lo_menu_lzo STATIC EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/thirdparty/lzokay/lzokay.cpp)
target_include_directories(lo_menu_lzo PUBLIC ${CMAKE_SOURCE_DIR}/thirdparty/lzokay)
target_compile_features(lo_menu_lzo PRIVATE cxx_std_17)
function(lo_add_header_test name source)
    add_executable(${name} EXCLUDE_FROM_ALL ${source})
    target_include_directories(${name} PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(${name} PRIVATE cxx_std_20)
    target_compile_definitions(${name} PRIVATE NOMINMAX)
    if(MSVC)
        target_compile_options(${name} PRIVATE /UNDEBUG)
    else()
        target_compile_options(${name} PRIVATE -UNDEBUG)
    endif()
endfunction()
if(NOT ANDROID)
lo_add_header_test(LoPollWaitTest ${CMAKE_SOURCE_DIR}/tools/tests/poll_wait_test.cpp)
lo_add_header_test(LoMenuAnimationsTest ${CMAKE_SOURCE_DIR}/tools/tests/menu_animations_test.cpp)
lo_add_header_test(LoHdrOutputTest ${CMAKE_SOURCE_DIR}/tools/tests/hdr_output_test.cpp)
lo_add_header_test(LoShadowResolutionTest ${CMAKE_SOURCE_DIR}/tools/tests/shadow_resolution_test.cpp)
lo_add_header_test(LoHdrSceneShaderTest ${CMAKE_SOURCE_DIR}/tools/tests/hdr_scene_shader_test.cpp)
lo_add_header_test(LoTimeFieldsTest ${CMAKE_SOURCE_DIR}/tools/tests/time_fields_test.cpp)
lo_add_header_test(LoLoadingStateTest ${CMAKE_SOURCE_DIR}/tools/tests/loading_state_test.cpp)
lo_add_header_test(LoSoftResetTest ${CMAKE_SOURCE_DIR}/tools/tests/soft_reset_test.cpp)
lo_add_header_test(LoNotifiedWaitTest ${CMAKE_SOURCE_DIR}/tools/tests/notified_wait_test.cpp)
lo_add_header_test(LoFrameGenerationPresentTest ${CMAKE_SOURCE_DIR}/tools/tests/frame_generation_present_test.cpp)
lo_add_header_test(LoMetalFxFgPolicyTest ${CMAKE_SOURCE_DIR}/tools/tests/streamline_fg/metalfx_policy_test.cpp)
target_compile_definitions(LoMetalFxFgPolicyTest PRIVATE LO_ENABLE_METALFX_FG=1)
lo_add_header_test(LoFsrQueuePlanTest ${CMAKE_SOURCE_DIR}/tools/tests/streamline_fg/fsr_queue_plan_test.cpp)
lo_add_header_test(LoFrameGenerationSettingsTest ${CMAKE_SOURCE_DIR}/tools/tests/frame_generation_settings_test.cpp)
target_compile_definitions(LoFrameGenerationSettingsTest PRIVATE LO_ENABLE_D3D12_FG=1 FRAMEGEN_WITH_DLSS=1 FRAMEGEN_WITH_FSR=1)
lo_add_header_test(LoShaderPrebuildIoTest ${CMAKE_SOURCE_DIR}/tools/tests/shader_prebuild_io_test.cpp)
lo_add_header_test(LoShaderPreparationRequestTest ${CMAKE_SOURCE_DIR}/tools/tests/shader_preparation_request_test.cpp)
lo_add_header_test(LoPlumeLogTest ${CMAKE_SOURCE_DIR}/tools/tests/plume_log_test.cpp)
target_include_directories(LoPlumeLogTest PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/plume)
lo_add_header_test(LoParticleMaterialCompatTest ${CMAKE_SOURCE_DIR}/tools/tests/particle_material_compat_test.cpp)
lo_add_header_test(LoRenderBatchPolicyTest ${CMAKE_SOURCE_DIR}/tools/tests/render_batch_policy_test.cpp)
lo_add_header_test(LoTextureDescriptorCacheTest ${CMAKE_SOURCE_DIR}/tools/tests/texture_descriptor_cache_test.cpp)
lo_add_header_test(LoTextureKeyTest ${CMAKE_SOURCE_DIR}/tools/tests/texture_key_test.cpp)
lo_add_header_test(LoTextureContentTest ${CMAKE_SOURCE_DIR}/tools/tests/texture_content_test.cpp)
target_link_libraries(LoTextureContentTest PRIVATE xxHash::xxhash)
lo_add_header_test(LoBcDecodeTest ${CMAKE_SOURCE_DIR}/tools/tests/bc_decode_test.cpp)
find_package(Threads REQUIRED)
lo_add_header_test(LoAudioCallbackTest ${CMAKE_SOURCE_DIR}/tools/tests/audio_callback_test.cpp)
target_link_libraries(LoAudioCallbackTest PRIVATE Threads::Threads)
lo_add_header_test(LoSettingsPathTest ${CMAKE_SOURCE_DIR}/tools/tests/settings_path_test.cpp)
target_sources(LoSettingsPathTest PRIVATE ${LO_RUNTIME_ROOT}/updater/update.cpp)
lo_add_header_test(LoExternalUpdateNoticeTest ${CMAKE_SOURCE_DIR}/tools/tests/external_update_notice_test.cpp)
if(MSVC)
    target_compile_options(LoExternalUpdateNoticeTest PRIVATE /utf-8)
endif()
lo_add_header_test(LoImportGameTest ${CMAKE_SOURCE_DIR}/tools/tests/import_game_test.cpp)
lo_add_header_test(LoCompactionPromptTest ${CMAKE_SOURCE_DIR}/tools/tests/compaction_prompt_test.cpp)
if(MSVC)
    target_compile_options(LoCompactionPromptTest PRIVATE /utf-8)
endif()
lo_add_header_test(LoImportIsoLocatorTest ${CMAKE_SOURCE_DIR}/tools/tests/import_iso_locator_test.cpp)
target_sources(LoImportIsoLocatorTest PRIVATE ${LO_RUNTIME_ROOT}/install/import_image.cpp)
lo_add_header_test(LoInstallerControllerTest ${CMAKE_SOURCE_DIR}/tools/tests/installer_controller_test.cpp)
lo_add_header_test(LoInstallerReviewPreviewTest ${CMAKE_SOURCE_DIR}/tools/tests/installer_review_preview_test.cpp)
target_sources(LoInstallerReviewPreviewTest PRIVATE
    ${LO_RUNTIME_ROOT}/install/installer_ui.cpp
    ${LO_RUNTIME_ROOT}/install/installer_font.cpp)
target_include_directories(LoInstallerReviewPreviewTest PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/SDL/include)
target_compile_definitions(LoInstallerReviewPreviewTest PRIVATE LO_INSTALLER_UI_TESTING SDL_MAIN_HANDLED)
target_link_libraries(LoInstallerReviewPreviewTest PRIVATE SDL3::SDL3-static)
lo_add_header_test(LoInstallHostTest ${CMAKE_SOURCE_DIR}/tools/tests/install_host_test.cpp)
target_sources(LoInstallHostTest PRIVATE ${LO_RUNTIME_ROOT}/install/host.cpp)
lo_add_header_test(LoFileBrowserFolderTest ${CMAKE_SOURCE_DIR}/tools/tests/file_browser_folder_test.cpp)
target_compile_definitions(LoImportGameTest PRIVATE LO_IMPORT_TESTING)
target_sources(LoImportGameTest PRIVATE
    ${LO_RUNTIME_ROOT}/install/import_game.cpp
    ${LO_RUNTIME_ROOT}/install/import_image.cpp)
if(WIN32)
    target_link_libraries(LoImportGameTest PRIVATE bcrypt)
endif()
lo_add_header_test(LoCapturedShaderTest ${CMAKE_SOURCE_DIR}/tools/tests/captured_shader_test.cpp)
lo_add_header_test(LoVertexCacheTest ${CMAKE_SOURCE_DIR}/tools/tests/vertex_cache_test.cpp)
lo_add_header_test(LoResolveCopyPolicyTest ${CMAKE_SOURCE_DIR}/tools/tests/resolve_copy_policy_test.cpp)
target_include_directories(LoVertexCacheTest PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/unordered_dense/include)
lo_add_header_test(LoRenderTimingTest ${CMAKE_SOURCE_DIR}/tools/tests/render_timing_test.cpp)
    lo_add_header_test(LoRenderArenaTest ${CMAKE_SOURCE_DIR}/tools/tests/render_arena_policy_test.cpp)
add_executable(LoUpdaterSdlUiTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/updater_sdl_ui_test.cpp
    ${LO_RUNTIME_ROOT}/install/installer_font.cpp
    ${LO_RUNTIME_ROOT}/updater/posix_ui.cpp
    ${LO_RUNTIME_ROOT}/updater/update.cpp)
target_include_directories(LoUpdaterSdlUiTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoUpdaterSdlUiTest PRIVATE cxx_std_20)
target_link_libraries(LoUpdaterSdlUiTest PRIVATE SDL3::SDL3-static)
if(MSVC)
    target_compile_options(LoUpdaterSdlUiTest PRIVATE /utf-8 /UNDEBUG)
else()
    target_compile_options(LoUpdaterSdlUiTest PRIVATE -UNDEBUG)
endif()
if(NOT WIN32)
    add_executable(LoUpdaterSdlProductionTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_sdl_ui_test.cpp
        ${LO_RUNTIME_ROOT}/install/installer_font.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp
        ${LO_RUNTIME_ROOT}/updater/update.cpp)
    target_include_directories(LoUpdaterSdlProductionTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterSdlProductionTest PRIVATE cxx_std_20)
    target_compile_options(LoUpdaterSdlProductionTest PRIVATE -UNDEBUG)
    target_link_libraries(LoUpdaterSdlProductionTest PRIVATE SDL3::SDL3-static)
endif()
add_executable(LoUpdaterGamePromptTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/updater_game_prompt_test.cpp
    ${LO_RUNTIME_ROOT}/updater/game_prompt.cpp)
target_include_directories(LoUpdaterGamePromptTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoUpdaterGamePromptTest PRIVATE cxx_std_20)
target_compile_definitions(LoUpdaterGamePromptTest PRIVATE SDL_MAIN_HANDLED)
target_link_libraries(LoUpdaterGamePromptTest PRIVATE SDL3::SDL3-static)
endif()

set(LO_PRIVATE_DIR ${CMAKE_SOURCE_DIR}/LostOdysseyRecompLib/private)

# Logging stubs for every kernel import without an implementation. Regenerated
# whenever a runtime source changes so newly implemented imports drop out.
file(GLOB_RECURSE LO_RUNTIME_SOURCES CONFIGURE_DEPENDS
    "${LO_RUNTIME_ROOT}/*.cpp"
    "${LO_RUNTIME_ROOT}/*.h")
list(FILTER LO_RUNTIME_SOURCES EXCLUDE REGEX "imports_stubs\\.cpp$")
# Discord Rich Presence is a desktop host service.
if(ANDROID OR LO_TARGET_PLATFORM STREQUAL "switch")
    list(FILTER LO_RUNTIME_SOURCES EXCLUDE REGEX "/discord/")
endif()
list(FILTER LO_RUNTIME_SOURCES EXCLUDE REGEX "updater/posix_ui\\.cpp$")
list(FILTER LO_RUNTIME_SOURCES EXCLUDE REGEX "gpu/shader/portable_shader_pack\\.cpp$")

set(LO_IMPORT_STUBS "${LO_RUNTIME_ROOT}/kernel/imports_stubs.cpp")
# macOS provides only python3; resolve the interpreter rather than assuming "python".
find_package(Python3 COMPONENTS Interpreter REQUIRED)
add_custom_command(
    OUTPUT ${LO_IMPORT_STUBS}
    COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/gen_import_stubs.py"
            "${LO_PRIVATE_DIR}/image_disc1.bin.sym" "${LO_RUNTIME_ROOT}" "${LO_IMPORT_STUBS}"
    DEPENDS ${LO_RUNTIME_SOURCES} "${CMAKE_SOURCE_DIR}/tools/gen_import_stubs.py" "${LO_PRIVATE_DIR}/image_disc1.bin.sym"
    COMMENT "Generating kernel import stubs"
    VERBATIM)

if(ANDROID)
    add_library(LostOdysseyRecomp SHARED ${LO_RUNTIME_SOURCES} ${LO_IMPORT_STUBS})
    set_target_properties(LostOdysseyRecomp PROPERTIES OUTPUT_NAME main)
    target_link_options(LostOdysseyRecomp PRIVATE "-Wl,-z,max-page-size=16384")
    # Calls between the library's own functions (the translated guest code
    # calls itself constantly) bind directly instead of going through the PLT.
    target_link_options(LostOdysseyRecomp PRIVATE "-Wl,-Bsymbolic-functions")
    # libadrenotools: optional custom Vulkan driver (Mesa Turnip) loading on
    # Qualcomm devices, selected at runtime with LO_VK_CUSTOM_DRIVER.
    if(EXISTS "${CMAKE_SOURCE_DIR}/thirdparty/libadrenotools/CMakeLists.txt")
        add_subdirectory("${CMAKE_SOURCE_DIR}/thirdparty/libadrenotools" "${CMAKE_BINARY_DIR}/adrenotools")
        target_link_libraries(LostOdysseyRecomp PRIVATE adrenotools)
        target_compile_definitions(LostOdysseyRecomp PRIVATE LO_HAS_ADRENOTOOLS=1)
        add_dependencies(LostOdysseyRecomp main_hook file_redirect_hook gsl_alloc_hook hook_impl)
    endif()
else()
    add_executable(LostOdysseyRecomp ${LO_RUNTIME_SOURCES} ${LO_IMPORT_STUBS})
    lo_stage_portable_shader_pack(LostOdysseyRecomp)
    add_custom_command(TARGET LostOdysseyRecomp POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CMAKE_SOURCE_DIR}/assets/steam" "$<TARGET_FILE_DIR:LostOdysseyRecomp>/assets/steam"
        COMMENT "Staging Steam library artwork" VERBATIM)
endif()
set(LO_BUILD_REVISION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/lo_build_revision.h")
add_custom_target(LoBuildRevision
    COMMAND ${CMAKE_COMMAND} "-DSOURCE_ROOT=${CMAKE_SOURCE_DIR}" "-DOUTPUT=${LO_BUILD_REVISION_HEADER}"
        -P "${CMAKE_SOURCE_DIR}/tools/write_build_revision.cmake"
    BYPRODUCTS "${LO_BUILD_REVISION_HEADER}" VERBATIM)
add_dependencies(LostOdysseyRecomp LoBuildRevision)
target_include_directories(LostOdysseyRecomp PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
# The game files rebuilt for model and text mods are cached under a key that
# includes a hash of the code building them (modding/ and the readers it uses):
# a release that leaves that code alone keeps the cache, any change rebuilds it.
set(LO_MODS_BUILDER_SOURCES ${LO_RUNTIME_SOURCES})
list(FILTER LO_MODS_BUILDER_SOURCES INCLUDE REGEX "/modding/")
# The asset exporter (--export-assets, the MO2 tool) rebuilds nothing.
list(FILTER LO_MODS_BUILDER_SOURCES EXCLUDE REGEX "/modding/(asset_export|gltf_writer|model_preview)\\.")
list(APPEND LO_MODS_BUILDER_SOURCES "${LO_RUNTIME_ROOT}/gpu/shader/cpx_decode.h" "${LO_RUNTIME_ROOT}/os/json.h")
set(LO_MODS_BUILDER_LIST "${CMAKE_CURRENT_BINARY_DIR}/lo_mods_builder_sources.txt")
list(JOIN LO_MODS_BUILDER_SOURCES "\n" LO_MODS_BUILDER_LINES)
file(CONFIGURE OUTPUT "${LO_MODS_BUILDER_LIST}" CONTENT "${LO_MODS_BUILDER_LINES}\n" @ONLY)
set(LO_MODS_BUILDER_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/lo_mods_builder.h")
add_custom_command(OUTPUT "${LO_MODS_BUILDER_HEADER}"
    COMMAND ${CMAKE_COMMAND} "-DSOURCE_ROOT=${CMAKE_SOURCE_DIR}" "-DSOURCES=${LO_MODS_BUILDER_LIST}"
        "-DOUTPUT=${LO_MODS_BUILDER_HEADER}" -P "${CMAKE_SOURCE_DIR}/tools/write_mods_builder_hash.cmake"
    DEPENDS ${LO_MODS_BUILDER_SOURCES} "${LO_MODS_BUILDER_LIST}" "${CMAKE_SOURCE_DIR}/tools/write_mods_builder_hash.cmake"
    COMMENT "Hashing the mods rebuild code" VERBATIM)
target_sources(LostOdysseyRecomp PRIVATE "${LO_MODS_BUILDER_HEADER}")
# Publish source identity only after the corresponding runtime links successfully.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/source-version.build.txt" CONTENT "${LO_SOURCE_VERSION}\n")
add_custom_command(TARGET LostOdysseyRecomp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "${CMAKE_CURRENT_BINARY_DIR}/source-version.build.txt"
        "$<TARGET_FILE_DIR:LostOdysseyRecomp>/source-version.txt" VERBATIM)
if(WIN32)
    enable_language(RC)
    target_sources(LostOdysseyRecomp PRIVATE ${CMAKE_SOURCE_DIR}/assets/lost-odyssey-recomp.rc)
    target_include_directories(LostOdysseyRecomp PRIVATE ${CMAKE_SOURCE_DIR}/assets)
    # Keep the existing main/CRT initialization while avoiding a console on double-click.
    target_link_options(LostOdysseyRecomp PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
    # Retain a build-local address map for module/RVA crash reports from
    # optimized packages without PDBs. The release payload excludes this file.
    target_link_options(LostOdysseyRecomp PRIVATE "/MAP:${CMAKE_CURRENT_BINARY_DIR}/LostOdysseyRecomp.map")
endif()

target_include_directories(LostOdysseyRecomp PRIVATE
    ${LO_RUNTIME_ROOT}
    ${CMAKE_SOURCE_DIR}/LostOdysseyRecompLib
    ${CMAKE_SOURCE_DIR}/tools/XenonRecomp/XenonUtils
    ${CMAKE_SOURCE_DIR}/tools/XenonRecomp/thirdparty/simde
    ${CMAKE_SOURCE_DIR}/thirdparty/unordered_dense/include
    ${CMAKE_SOURCE_DIR}/thirdparty/o1heap/o1heap
    # miniz.h for modding/asset_export.cpp; updater/stage.cpp holds the definitions.
    ${LO_MINIZ_DIR}
    # gpu/shader/dxc_compiler.cpp is part of every runtime build, GPU or not.
    ${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/inc)

target_precompile_headers(LostOdysseyRecomp PRIVATE stdafx.h)
target_compile_features(LostOdysseyRecomp PRIVATE cxx_std_20)
target_compile_options(LostOdysseyRecomp PRIVATE -fno-strict-aliasing -Wno-switch -Wno-unused-function)
target_compile_definitions(LostOdysseyRecomp PRIVATE NOMINMAX LO_SOURCE_VERSION="${LO_SOURCE_VERSION}")

target_link_libraries(LostOdysseyRecomp PRIVATE
    lo_portable_shader_pack
    lo_menu_lzo
    LostOdysseyRecompLib
    XenonUtils
    fmt::fmt
    xxHash::xxhash
    o1heap
    SDL3::SDL3)
if(ANDROID)
    target_link_libraries(LostOdysseyRecomp PRIVATE android log dl nlohmann_json::nlohmann_json)
elseif(UNIX)
    set_target_properties(LostOdysseyRecomp PROPERTIES
        INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib")
    find_package(CURL REQUIRED)
    target_link_libraries(LostOdysseyRecomp PRIVATE pthread dl)
    target_link_libraries(LostOdysseyRecomp PRIVATE CURL::libcurl)
endif()
target_link_libraries(LostOdysseyRecomp PRIVATE lo_avcodec)

if(LO_BUILD_GPU)
    target_link_libraries(LostOdysseyRecomp PRIVATE plume)
    target_include_directories(LostOdysseyRecomp PRIVATE
        ${CMAKE_SOURCE_DIR}/thirdparty/plume)
    target_compile_definitions(LostOdysseyRecomp PRIVATE LO_GPU_PLUME=1)
    if(APPLE)
        target_include_directories(LostOdysseyRecomp PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/plume/contrib/metal-cpp)
    endif()
    if(WIN32)
        # Vulkan runs never load the D3D12 runtime; video.cpp probes both DLLs
        # before the D3D12 attempt so a missing one falls back instead of faulting.
        # d3d11/mfplat serve only replacement cutscene movies. A static d3d11
        # import pulls in dxgi.dll at process start, before main.cpp can keep a
        # ReShade dxgi.dll out of Vulkan runs (#506).
        target_link_libraries(LostOdysseyRecomp PRIVATE d3d12 dxgi delayimp)
        target_link_options(LostOdysseyRecomp PRIVATE /DELAYLOAD:d3d12.dll /DELAYLOAD:dxgi.dll
            /DELAYLOAD:d3d11.dll /DELAYLOAD:mfplat.dll)
    endif()
    lo_enable_dlss(LostOdysseyRecomp)
    lo_enable_streamline_fg(LostOdysseyRecomp)
    lo_enable_d3d12_fg(LostOdysseyRecomp)
    lo_enable_vulkan_fsr_fg(LostOdysseyRecomp)
    lo_enable_metalfx_fg(LostOdysseyRecomp)
    if(LO_ENABLE_STREAMLINE_FG)
        lo_add_header_test(LoDlssFgConstantsTest ${CMAKE_SOURCE_DIR}/tools/tests/streamline_fg/fg_constants_test.cpp)
        target_include_directories(LoDlssFgConstantsTest PRIVATE "${LO_STREAMLINE_SDK_ROOT}/include")
    endif()
    lo_enable_fsr(LostOdysseyRecomp)
    lo_enable_xess(LostOdysseyRecomp)
endif()

if(LO_RENDERER_P2_SELFTEST)
    target_compile_definitions(LostOdysseyRecomp PRIVATE LO_RENDERER_P2_SELFTEST=1)
endif()

if(NOT ANDROID)
if(WIN32)
    target_link_libraries(LostOdysseyRecomp PRIVATE winmm onecore bcrypt winhttp ole32 shell32 advapi32)

    lo_add_header_test(LoUpdaterApplyArgumentsTest ${CMAKE_SOURCE_DIR}/tools/tests/updater_apply_arguments_test.cpp)
    target_link_libraries(LoUpdaterApplyArgumentsTest PRIVATE shell32)

    set(LO_UPDATER_TRANSACTION_SOURCES
        ${LO_RUNTIME_ROOT}/updater/update.cpp
        ${LO_RUNTIME_ROOT}/updater/stage.cpp
        ${LO_RUNTIME_ROOT}/updater/apply.cpp)
    add_executable(LoUpdaterHttpFailureTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_http_failure_test.cpp ${LO_UPDATER_TRANSACTION_SOURCES})
    target_include_directories(LoUpdaterHttpFailureTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterHttpFailureTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterHttpFailureTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterHttpFailureTest PRIVATE shell32)
    add_executable(LoUpdaterStartupOrderTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_startup_order_test.cpp
        ${LO_RUNTIME_ROOT}/updater/update.cpp
        ${LO_RUNTIME_ROOT}/updater/windows_http.cpp
        ${LO_RUNTIME_ROOT}/updater/stage.cpp
        ${LO_RUNTIME_ROOT}/updater/apply.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp)
    target_include_directories(LoUpdaterStartupOrderTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterStartupOrderTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterStartupOrderTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterStartupOrderTest PRIVATE winhttp user32 gdi32 shell32)
    add_executable(LostOdysseyUpdater WIN32
        ${CMAKE_SOURCE_DIR}/tools/updater/helper_main.cpp
        ${LO_RUNTIME_ROOT}/updater/apply_mode.cpp
        ${LO_RUNTIME_ROOT}/updater/standalone.cpp
        ${LO_RUNTIME_ROOT}/updater/windows_http.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp
        ${CMAKE_SOURCE_DIR}/assets/lost-odyssey-recomp.rc
        ${LO_UPDATER_TRANSACTION_SOURCES})
    target_include_directories(LostOdysseyUpdater PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/assets)
    target_compile_features(LostOdysseyUpdater PRIVATE cxx_std_20)
    target_compile_definitions(LostOdysseyUpdater PRIVATE NOMINMAX)
    target_compile_definitions(LostOdysseyUpdater PRIVATE LO_SOURCE_VERSION="${LO_SOURCE_VERSION}")
    target_link_libraries(LostOdysseyUpdater PRIVATE winhttp user32 gdi32 shell32)
    # The runtime owns the update path; the standalone helper remains available
    # only for focused compatibility fixtures.
    add_executable(LoUpdaterProbe WIN32 EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/updater_probe.cpp)
    target_compile_features(LoUpdaterProbe PRIVATE cxx_std_20)
    target_link_libraries(LoUpdaterProbe PRIVATE shell32)

    add_executable(LoUpdaterTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_test.cpp
        ${LO_UPDATER_TRANSACTION_SOURCES}
        ${LO_RUNTIME_ROOT}/updater/windows_http.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp)
    target_include_directories(LoUpdaterTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterTest PRIVATE winhttp user32 gdi32 shell32)
    add_dependencies(LoUpdaterTest LostOdysseyUpdater LoUpdaterProbe)

    add_executable(LoUpdaterProgressTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_progress_test.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp)
    target_include_directories(LoUpdaterProgressTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterProgressTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterProgressTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterProgressTest PRIVATE user32 gdi32)

    add_executable(LoUpdaterStandaloneTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_standalone_test.cpp
        ${LO_UPDATER_TRANSACTION_SOURCES}
        ${LO_RUNTIME_ROOT}/updater/standalone.cpp
        ${LO_RUNTIME_ROOT}/updater/windows_http.cpp
        ${LO_RUNTIME_ROOT}/updater/progress.cpp)
    target_include_directories(LoUpdaterStandaloneTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterStandaloneTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterStandaloneTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterStandaloneTest PRIVATE winhttp user32 gdi32 shell32)
    add_dependencies(LoUpdaterStandaloneTest LostOdysseyUpdater LoUpdaterProbe)

    add_executable(LoUpdaterHelperContextTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/updater_helper_context_test.cpp
        ${LO_RUNTIME_ROOT}/updater/update.cpp)
    target_include_directories(LoUpdaterHelperContextTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoUpdaterHelperContextTest PRIVATE cxx_std_20)
    target_compile_definitions(LoUpdaterHelperContextTest PRIVATE NOMINMAX)
    target_link_libraries(LoUpdaterHelperContextTest PRIVATE user32 shell32)
    add_dependencies(LoUpdaterHelperContextTest LostOdysseyUpdater LoUpdaterProbe)
endif()

# Host-only restart argument and parent/child lifetime checks.
if(WIN32)
    add_executable(LoFolderPickerTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/folder_picker_test.cpp)
    target_include_directories(LoFolderPickerTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoFolderPickerTest PRIVATE cxx_std_20)
    target_compile_definitions(LoFolderPickerTest PRIVATE NOMINMAX)
    target_compile_options(LoFolderPickerTest PRIVATE /utf-8 /UNDEBUG)
    target_link_libraries(LoFolderPickerTest PRIVATE user32 ole32 shell32)

    add_executable(LoGameWindowPixelsTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/game_window_pixels_test.cpp)
    target_compile_features(LoGameWindowPixelsTest PRIVATE cxx_std_20)
    target_compile_options(LoGameWindowPixelsTest PRIVATE /utf-8 /W4 /WX)
    target_link_libraries(LoGameWindowPixelsTest PRIVATE SDL3::SDL3-static user32)
    set_target_properties(LoGameWindowPixelsTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)

    add_executable(LoRestartTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/restart_test.cpp)
    target_include_directories(LoRestartTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoRestartTest PRIVATE cxx_std_20)
    target_compile_definitions(LoRestartTest PRIVATE NOMINMAX)
    target_link_libraries(LoRestartTest PRIVATE user32 shell32)
    set_target_properties(LoRestartTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
endif()

# Current-process log snapshots without guest generation or GPU access.
add_executable(LoLogCaptureTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/log_capture_test.cpp
    ${LO_RUNTIME_ROOT}/os/log_file.cpp)
target_include_directories(LoLogCaptureTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoLogCaptureTest PRIVATE cxx_std_20)
target_link_libraries(LoLogCaptureTest PRIVATE fmt::fmt)

# Model replacements (modding/model_import.h) on a package or through the archive
# overlay, without guest/GPU work.
add_executable(LoModelImport EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/model_import_cli.cpp
    ${LO_RUNTIME_ROOT}/modding/font_patch.cpp
    ${LO_RUNTIME_ROOT}/modding/gltf_reader.cpp
    ${LO_RUNTIME_ROOT}/modding/model_import.cpp
    ${LO_RUNTIME_ROOT}/modding/mod_api.cpp
    ${LO_RUNTIME_ROOT}/modding/text_format.cpp
    ${LO_RUNTIME_ROOT}/modding/text_menu.cpp
    ${LO_RUNTIME_ROOT}/modding/text_overlay.cpp)
target_include_directories(LoModelImport PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoModelImport PRIVATE cxx_std_20)
target_compile_definitions(LoModelImport PRIVATE NOMINMAX)
target_link_libraries(LoModelImport PRIVATE fmt::fmt lo_menu_lzo Threads::Threads)
set_target_properties(LoModelImport PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)

# Language-pack glyphs (modding/font_patch.h) on a font package from a disc,
# checked with the settings menu's reader, without guest/GPU work.
add_executable(LoFontPatch EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/font_patch_cli.cpp
    ${LO_RUNTIME_ROOT}/modding/font_patch.cpp
    ${LO_RUNTIME_ROOT}/modding/mod_api.cpp
    ${LO_RUNTIME_ROOT}/settings/menu_assets.cpp)
target_include_directories(LoFontPatch PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoFontPatch PRIVATE cxx_std_20)
target_compile_definitions(LoFontPatch PRIVATE NOMINMAX)
target_link_libraries(LoFontPatch PRIVATE lo_menu_lzo Threads::Threads)
set_target_properties(LoFontPatch PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)

# Background archive publication and failure recovery, without guest/GPU work.
if(WIN32)
    add_executable(LoCaptureArchiveTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/capture_archive_test.cpp
        ${LO_RUNTIME_ROOT}/os/capture_archive.cpp)
    target_include_directories(LoCaptureArchiveTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoCaptureArchiveTest PRIVATE cxx_std_20)

    # Shader-log routing, concurrent snapshots, real compiler diagnostics and ZIP
    # inclusion; no GPU, game assets or generated guest library.
    add_executable(LoShaderLogTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/shader_log_test.cpp
        ${LO_RUNTIME_ROOT}/os/log_file.cpp
        ${LO_RUNTIME_ROOT}/os/capture_archive.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShaderLogTest PRIVATE ${LO_RUNTIME_ROOT}
        ${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/inc)
    target_compile_features(LoShaderLogTest PRIVATE cxx_std_20)
    target_compile_definitions(LoShaderLogTest PRIVATE NOMINMAX)
    target_link_libraries(LoShaderLogTest PRIVATE fmt::fmt)
endif()

# Crash reporting is exercised in sacrificial child processes, without the
# guest library or a GPU. The fixture supplies inert guest-memory state.
if(WIN32)
    add_executable(LoCrashCaptureTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/crash_capture_test.cpp
        ${LO_RUNTIME_ROOT}/os/crash_handler.cpp
        ${LO_RUNTIME_ROOT}/os/log_file.cpp)
    target_include_directories(LoCrashCaptureTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoCrashCaptureTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
    target_compile_options(LoCrashCaptureTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
    target_compile_features(LoCrashCaptureTest PRIVATE cxx_std_20)
    target_link_libraries(LoCrashCaptureTest PRIVATE fmt::fmt xxHash::xxhash dbghelp)
endif()

# Integration test uses the actual guest import bridge and storage implementation.
set(LO_STORAGE_TEST_SOURCES ${LO_RUNTIME_SOURCES})
list(FILTER LO_STORAGE_TEST_SOURCES EXCLUDE REGEX "/main\\.cpp$")
add_executable(LoStorageTest EXCLUDE_FROM_ALL ${LO_STORAGE_TEST_SOURCES} ${LO_IMPORT_STUBS}
    ${CMAKE_SOURCE_DIR}/tools/tests/storage_test.cpp)
target_include_directories(LoStorageTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
target_compile_definitions(LoStorageTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS> LO_STORAGE_TESTING)
target_compile_options(LoStorageTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
target_compile_features(LoStorageTest PRIVATE cxx_std_20)
target_precompile_headers(LoStorageTest REUSE_FROM LostOdysseyRecomp)
target_link_libraries(LoStorageTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,LINK_LIBRARIES>)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # Keep the manual diagnostic entry point callable even in stripped packages.
    target_link_options(LostOdysseyRecomp PRIVATE "-Wl,--export-dynamic-symbol=LoDumpIoDiagnostics")
    target_link_options(LoStorageTest PRIVATE "-Wl,--export-dynamic-symbol=LoDumpIoDiagnostics")
endif()

# Actual generated function-table and script-handler regression for issue #5.
add_executable(LoGuestDispatchTest EXCLUDE_FROM_ALL ${LO_STORAGE_TEST_SOURCES} ${LO_IMPORT_STUBS}
    ${CMAKE_SOURCE_DIR}/tools/tests/guest_dispatch_test.cpp)
target_include_directories(LoGuestDispatchTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
target_compile_definitions(LoGuestDispatchTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
target_compile_options(LoGuestDispatchTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
target_compile_features(LoGuestDispatchTest PRIVATE cxx_std_20)
target_precompile_headers(LoGuestDispatchTest REUSE_FROM LostOdysseyRecomp)
target_link_libraries(LoGuestDispatchTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,LINK_LIBRARIES>)

# Guest string formatting regression without starting the game.
add_executable(LoGuestPrintfTest EXCLUDE_FROM_ALL
    ${LO_RUNTIME_ROOT}/kernel/guest_printf.cpp
    ${CMAKE_SOURCE_DIR}/tools/tests/guest_printf_test.cpp)
target_include_directories(LoGuestPrintfTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
target_compile_definitions(LoGuestPrintfTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
target_compile_options(LoGuestPrintfTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
target_compile_features(LoGuestPrintfTest PRIVATE cxx_std_20)
target_precompile_headers(LoGuestPrintfTest REUSE_FROM LostOdysseyRecomp)
target_link_libraries(LoGuestPrintfTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,LINK_LIBRARIES>)

# Offline shader translator check (shares gpu/shader with the runtime).
add_executable(LoHidTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/hid_test.cpp ${LO_RUNTIME_ROOT}/hid/hid.cpp)
if(ANDROID)
    target_sources(LoHidTest PRIVATE ${LO_RUNTIME_ROOT}/hid/android_touch.cpp)
endif()
target_include_directories(LoHidTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
target_compile_definitions(LoHidTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
target_compile_options(LoHidTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
target_compile_features(LoHidTest PRIVATE cxx_std_20)
set_target_properties(LoHidTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
if(ANDROID)
    target_link_libraries(LoHidTest PRIVATE fmt::fmt SDL3::SDL3 xxHash::xxhash)
else()
    target_link_libraries(LoHidTest PRIVATE fmt::fmt SDL3::SDL3-static xxHash::xxhash)
endif()

if(NOT ANDROID)
    # Compile the production HID merger's Android branch with host SDL virtual pads.
    add_executable(LoAndroidTouchInputTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/android_touch_input_test.cpp
        ${LO_RUNTIME_ROOT}/hid/android_touch.cpp)
    target_include_directories(LoAndroidTouchInputTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoAndroidTouchInputTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
    target_compile_options(LoAndroidTouchInputTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_OPTIONS>)
    target_compile_features(LoAndroidTouchInputTest PRIVATE cxx_std_20)
    set_target_properties(LoAndroidTouchInputTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    target_link_libraries(LoAndroidTouchInputTest PRIVATE fmt::fmt SDL3::SDL3-static xxHash::xxhash)
endif()

add_executable(LoShaderTool
    ${CMAKE_SOURCE_DIR}/tools/xenos_shader_tool/main.cpp
    ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
    ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShaderTool PRIVATE ${LO_RUNTIME_ROOT}
        ${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/inc)
target_compile_features(LoShaderTool PRIVATE cxx_std_20)
target_compile_definitions(LoShaderTool PRIVATE NOMINMAX)
target_link_libraries(LoShaderTool PRIVATE fmt::fmt)

# Translates and compiles HLSL with DXC, without a GPU or game assets. On macOS
# it loads the bundled libdxcompiler.dylib the way the runtime does.
if(WIN32 OR APPLE)
    add_executable(LoRenderResolutionShaderTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/render_resolution_shader_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoRenderResolutionShaderTest PRIVATE ${LO_RUNTIME_ROOT}
        ${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/inc)
    target_compile_features(LoRenderResolutionShaderTest PRIVATE cxx_std_20)
    target_compile_definitions(LoRenderResolutionShaderTest PRIVATE NOMINMAX)
    target_link_libraries(LoRenderResolutionShaderTest PRIVATE fmt::fmt)

    add_executable(LoShadowProjectionShaderTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/shadow_projection_shader_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShadowProjectionShaderTest PRIVATE ${LO_RUNTIME_ROOT}
        ${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/inc)
    target_compile_features(LoShadowProjectionShaderTest PRIVATE cxx_std_20)
    target_compile_definitions(LoShadowProjectionShaderTest PRIVATE NOMINMAX)
    target_link_libraries(LoShadowProjectionShaderTest PRIVATE fmt::fmt)
endif()

# Tests packed mip block origins without game assets.
add_executable(LoXmaLoopTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/xma_loop_test.cpp)
target_include_directories(LoXmaLoopTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoXmaLoopTest PRIVATE cxx_std_20)

add_executable(LoPipelineCacheTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/pipeline_cache_test.cpp)
target_include_directories(LoPipelineCacheTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoPipelineCacheTest PRIVATE cxx_std_20)

add_executable(LoCpxDecodeTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/cpx_decode_test.cpp)
target_include_directories(LoCpxDecodeTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoCpxDecodeTest PRIVATE cxx_std_20)

add_executable(LoShaderResourceVariantsTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/shader_resource_variants_test.cpp)
target_include_directories(LoShaderResourceVariantsTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoShaderResourceVariantsTest PRIVATE cxx_std_20)

add_executable(LoTextureLayoutTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/texture_layout_test.cpp)
target_include_directories(LoTextureLayoutTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoTextureLayoutTest PRIVATE cxx_std_20)

add_executable(LoDriverPipelineCacheTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/driver_pipeline_cache_test.cpp)
target_include_directories(LoDriverPipelineCacheTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoDriverPipelineCacheTest PRIVATE cxx_std_20)

add_executable(LoGamePathTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/game_path_test.cpp)
target_include_directories(LoGamePathTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoGamePathTest PRIVATE cxx_std_20)

add_executable(LoUserPathsTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/user_paths_test.cpp)
target_include_directories(LoUserPathsTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoUserPathsTest PRIVATE cxx_std_20)

# Tests the exact address mappings used by the runtime, without game assets.
add_executable(LoCriticalSectionTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/critical_section_test.cpp)
target_include_directories(LoCriticalSectionTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoCriticalSectionTest PRIVATE cxx_std_23)

add_executable(LoMemoryAliasTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/memory_alias_test.cpp
    ${LO_RUNTIME_ROOT}/kernel/guest_address_space.cpp
    ${LO_RUNTIME_ROOT}/kernel/guest_address_space_macos.cpp)
target_include_directories(LoMemoryAliasTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoMemoryAliasTest PRIVATE cxx_std_20)
if(WIN32)
    target_link_libraries(LoMemoryAliasTest PRIVATE onecore)
    # Includes the production allocator with native API fault replacements.
    add_executable(LoMemoryFailureTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/memory_failure_test.cpp)
    target_include_directories(LoMemoryFailureTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoMemoryFailureTest PRIVATE cxx_std_20)
    target_compile_definitions(LoMemoryFailureTest PRIVATE NOMINMAX)

    add_executable(LoShaderAluTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/shader_alu_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShaderAluTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoShaderAluTest PRIVATE cxx_std_20)
    target_compile_definitions(LoShaderAluTest PRIVATE NOMINMAX)
    target_link_libraries(LoShaderAluTest PRIVATE fmt::fmt d3d12 dxgi)

    add_executable(LoShaderLoopTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/shader_loop_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShaderLoopTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoShaderLoopTest PRIVATE cxx_std_20)
    target_compile_definitions(LoShaderLoopTest PRIVATE NOMINMAX)
    target_link_libraries(LoShaderLoopTest PRIVATE fmt::fmt d3d12 dxgi)
    if(LO_BUILD_GPU)
        add_executable(LoRenderResolutionGpuTest EXCLUDE_FROM_ALL
            ${CMAKE_SOURCE_DIR}/tools/tests/render_resolution_gpu_test.cpp
            ${LO_RUNTIME_ROOT}/os/log_file.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
        target_include_directories(LoRenderResolutionGpuTest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoRenderResolutionGpuTest PRIVATE cxx_std_20)
        target_compile_definitions(LoRenderResolutionGpuTest PRIVATE NOMINMAX)
        set_target_properties(LoRenderResolutionGpuTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
        target_link_libraries(LoRenderResolutionGpuTest PRIVATE plume d3d12 dxgi fmt::fmt)
        add_executable(LoResolveCopyGpuTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/resolve_copy_gpu_test.cpp)
        target_include_directories(LoResolveCopyGpuTest PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoResolveCopyGpuTest PRIVATE cxx_std_20)
        target_compile_definitions(LoResolveCopyGpuTest PRIVATE NOMINMAX)
        target_link_libraries(LoResolveCopyGpuTest PRIVATE plume d3d12 dxgi)
        add_executable(LoDepthClearGpuTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/depth_clear_gpu_test.cpp)
        target_include_directories(LoDepthClearGpuTest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoDepthClearGpuTest PRIVATE cxx_std_20)
        target_compile_definitions(LoDepthClearGpuTest PRIVATE NOMINMAX)
        target_link_libraries(LoDepthClearGpuTest PRIVATE plume d3d12 dxgi)
        add_executable(LoPresentationTest EXCLUDE_FROM_ALL
            ${CMAKE_SOURCE_DIR}/tools/tests/presentation_test.cpp
            ${LO_RUNTIME_ROOT}/gpu/presentation.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
        target_include_directories(LoPresentationTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
        target_compile_definitions(LoPresentationTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,COMPILE_DEFINITIONS>)
        target_compile_features(LoPresentationTest PRIVATE cxx_std_20)
        # Keep this focused GPU fixture independent of rebuilding the game.
        set_target_properties(LoPresentationTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
        target_link_libraries(LoPresentationTest PRIVATE plume d3d12 dxgi fmt::fmt)
        add_executable(LoTemporalAATest EXCLUDE_FROM_ALL
            ${CMAKE_SOURCE_DIR}/tools/tests/temporal_aa_test.cpp
            ${LO_RUNTIME_ROOT}/gpu/temporal_aa.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp
            ${CMAKE_SOURCE_DIR}/tools/tests/motion_replay/platform_stub.cpp)
        target_include_directories(LoTemporalAATest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoTemporalAATest PRIVATE cxx_std_20)
        target_compile_definitions(LoTemporalAATest PRIVATE LO_GPU_PLUME NOMINMAX)
        target_link_libraries(LoTemporalAATest PRIVATE plume d3d12 dxgi fmt::fmt)
        add_executable(LoAmbientOcclusionGpuTest EXCLUDE_FROM_ALL
            ${CMAKE_SOURCE_DIR}/tools/tests/ambient_occlusion_gpu_test.cpp
            ${LO_RUNTIME_ROOT}/gpu/ambient_occlusion.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
        target_include_directories(LoAmbientOcclusionGpuTest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoAmbientOcclusionGpuTest PRIVATE cxx_std_20)
        target_compile_definitions(LoAmbientOcclusionGpuTest PRIVATE LO_GPU_PLUME NOMINMAX)
        target_link_libraries(LoAmbientOcclusionGpuTest PRIVATE plume d3d12 dxgi fmt::fmt)
        add_executable(LoBloomPrefilterTest EXCLUDE_FROM_ALL
            ${CMAKE_SOURCE_DIR}/tools/tests/bloom_prefilter_test.cpp
            ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
        target_include_directories(LoBloomPrefilterTest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
        target_compile_features(LoBloomPrefilterTest PRIVATE cxx_std_20)
        target_compile_definitions(LoBloomPrefilterTest PRIVATE LO_GPU_PLUME NOMINMAX)
        target_link_libraries(LoBloomPrefilterTest PRIVATE plume d3d12 dxgi fmt::fmt)
    endif()
endif()

add_executable(LoDepthClearLayoutTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/depth_clear_layout_test.cpp)
target_include_directories(LoDepthClearLayoutTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoDepthClearLayoutTest PRIVATE cxx_std_20)

add_executable(LoPendingClearTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/pending_clear_test.cpp)
target_include_directories(LoPendingClearTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoPendingClearTest PRIVATE cxx_std_20)

# Host-only fixtures do not require guest generation or the runtime PCH.
add_executable(LoRegisterSnapshotTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/register_snapshot_test.cpp)
target_include_directories(LoRegisterSnapshotTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoRegisterSnapshotTest PRIVATE cxx_std_20)

add_executable(LoPositionEvidenceTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/position_evidence_test.cpp)
target_compile_features(LoPositionEvidenceTest PRIVATE cxx_std_20)

add_executable(LoRenderResolutionTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/render_resolution_test.cpp)
target_include_directories(LoRenderResolutionTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoRenderResolutionTest PRIVATE cxx_std_20)

add_executable(LoAspectLayoutTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/aspect_layout_test.cpp)
target_include_directories(LoAspectLayoutTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoAspectLayoutTest PRIVATE cxx_std_20)

add_executable(LoEncounterDeferTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/encounter_defer_test.cpp)
target_include_directories(LoEncounterDeferTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoEncounterDeferTest PRIVATE cxx_std_20)

add_executable(LoFsrProjectionTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/fsr_projection_test.cpp)
target_include_directories(LoFsrProjectionTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoFsrProjectionTest PRIVATE cxx_std_20)

add_executable(LoFramePlanTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/frame_plan_test.cpp ${LO_RUNTIME_ROOT}/gpu/upscaling_plan.cpp)
target_include_directories(LoFramePlanTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoFramePlanTest PRIVATE cxx_std_20)

# CPU contract fixtures. They do not require Plume, D3D12, or guest code, so
# Linux configures the same targets.
add_executable(LoDlssStatusLogTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/dlss_status_log_test.cpp)
target_include_directories(LoDlssStatusLogTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoDlssStatusLogTest PRIVATE cxx_std_20)
target_compile_definitions(LoDlssStatusLogTest PRIVATE NOMINMAX)
target_link_libraries(LoDlssStatusLogTest PRIVATE fmt::fmt)

if(WIN32)
    add_executable(LoHangWatchTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/hang_watch_test.cpp)
    target_include_directories(LoHangWatchTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoHangWatchTest PRIVATE cxx_std_20)
    target_compile_definitions(LoHangWatchTest PRIVATE NOMINMAX)
    target_link_libraries(LoHangWatchTest PRIVATE fmt::fmt)
endif()

add_executable(LoTargetMappingTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/target_mapping_test.cpp)
target_include_directories(LoTargetMappingTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoTargetMappingTest PRIVATE cxx_std_20)

add_executable(LoTemporalSceneTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/temporal_scene_test.cpp)
target_include_directories(LoTemporalSceneTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoTemporalSceneTest PRIVATE cxx_std_20)

add_executable(LoTemporalJitterTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/temporal_jitter_test.cpp)
target_include_directories(LoTemporalJitterTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoTemporalJitterTest PRIVATE cxx_std_20)

add_executable(LoTemporalMathTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/temporal_math_test.cpp)
target_include_directories(LoTemporalMathTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoTemporalMathTest PRIVATE cxx_std_20)

add_executable(LoMotionVectorTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/motion_vector_test.cpp)
target_include_directories(LoMotionVectorTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoMotionVectorTest PRIVATE cxx_std_20)

add_executable(LoFramePacerTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/frame_pacer_test.cpp)
target_include_directories(LoFramePacerTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoFramePacerTest PRIVATE cxx_std_20)

add_executable(LoPresentationProgressTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/presentation_progress_test.cpp)
target_include_directories(LoPresentationProgressTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoPresentationProgressTest PRIVATE cxx_std_20)
target_link_libraries(LoPresentationProgressTest PRIVATE Threads::Threads)

add_executable(LoHostUiCompositeTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/host_ui_composite_test.cpp)
target_include_directories(LoHostUiCompositeTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoHostUiCompositeTest PRIVATE cxx_std_20)

add_executable(LoDebugOverlayTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/debug_overlay_test.cpp
    ${LO_RUNTIME_ROOT}/debug/menu_overlay.cpp)
target_include_directories(LoDebugOverlayTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoDebugOverlayTest PRIVATE cxx_std_20)
if(MSVC)
    target_compile_options(LoDebugOverlayTest PRIVATE /utf-8)
endif()

add_executable(LoDebugMenuInteractionTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/debug_menu_interaction_test.cpp
    ${LO_RUNTIME_ROOT}/debug/menu_overlay.cpp)
target_include_directories(LoDebugMenuInteractionTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoDebugMenuInteractionTest PRIVATE cxx_std_20)
if(MSVC)
    target_compile_options(LoDebugMenuInteractionTest PRIVATE /utf-8 /UNDEBUG)
endif()

if(WIN32)
    add_executable(LoSaveAnywhereConfigTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/save_anywhere_config_test.cpp)
    target_include_directories(LoSaveAnywhereConfigTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_features(LoSaveAnywhereConfigTest PRIVATE cxx_std_20)
    target_link_libraries(LoSaveAnywhereConfigTest PRIVATE fmt::fmt)
    if(MSVC)
        target_compile_options(LoSaveAnywhereConfigTest PRIVATE /utf-8)
    endif()
endif()

add_executable(LoSaveWorkerRequestTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/save_worker_request_test.cpp)
target_include_directories(LoSaveWorkerRequestTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
target_compile_features(LoSaveWorkerRequestTest PRIVATE cxx_std_20)
if(MSVC)
    target_compile_options(LoSaveWorkerRequestTest PRIVATE /utf-8)
endif()

add_executable(LoQuitTextHookTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/quit_text_hook_test.cpp)
target_include_directories(LoQuitTextHookTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoQuitTextHookTest PRIVATE cxx_std_20)

add_executable(LoQuitActionHookTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/quit_action_hook_test.cpp)
target_include_directories(LoQuitActionHookTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoQuitActionHookTest PRIVATE cxx_std_20)

add_executable(LoShaderPreparationQueueTest EXCLUDE_FROM_ALL
    ${CMAKE_SOURCE_DIR}/tools/tests/shader_preparation_queue_test.cpp)
target_include_directories(LoShaderPreparationQueueTest PRIVATE ${LO_RUNTIME_ROOT})
target_compile_features(LoShaderPreparationQueueTest PRIVATE cxx_std_20)
if(WIN32)
    add_executable(LoMenuRenderTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/menu_render_test.cpp
        ${LO_RUNTIME_ROOT}/settings/menu_assets.cpp
        ${LO_RUNTIME_ROOT}/settings/menu_render.cpp)
    target_include_directories(LoMenuRenderTest PRIVATE ${LO_RUNTIME_ROOT})
    target_compile_features(LoMenuRenderTest PRIVATE cxx_std_20)
    target_compile_options(LoMenuRenderTest PRIVATE /utf-8)
    target_compile_definitions(LoMenuRenderTest PRIVATE NOMINMAX LO_MENU_RENDER_TRACE)
    target_link_libraries(LoMenuRenderTest PRIVATE user32 gdi32 lo_menu_lzo xxHash::xxhash)

    add_executable(LoMenuFlowTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/menu_flow_test.cpp
        ${LO_RUNTIME_ROOT}/settings/menu_assets.cpp
        ${LO_RUNTIME_ROOT}/settings/menu_render.cpp)
    target_include_directories(LoMenuFlowTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES> ${CMAKE_SOURCE_DIR}/thirdparty/SDL/include)
    target_compile_features(LoMenuFlowTest PRIVATE cxx_std_20)
    target_compile_options(LoMenuFlowTest PRIVATE /utf-8)
    target_compile_definitions(LoMenuFlowTest PRIVATE NOMINMAX LO_GPU_PLUME LO_ENABLE_D3D12_FG FRAMEGEN_WITH_DLSS FRAMEGEN_WITH_FSR)
    target_link_libraries(LoMenuFlowTest PRIVATE user32 gdi32 lo_menu_lzo fmt::fmt xxHash::xxhash)
endif()

if(LO_BUILD_GPU)
    add_executable(LoBackendSelectionTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/backend_selection_test.cpp)
    target_include_directories(LoBackendSelectionTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoBackendSelectionTest PRIVATE NOMINMAX)
    target_compile_features(LoBackendSelectionTest PRIVATE cxx_std_20)
    set_target_properties(LoBackendSelectionTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    if(MSVC)
        target_compile_options(LoBackendSelectionTest PRIVATE /UNDEBUG /utf-8)
    else()
        target_compile_options(LoBackendSelectionTest PRIVATE -UNDEBUG)
    endif()
endif()

if(WIN32 AND LO_BUILD_GPU)
    add_executable(LoBackendDeviceTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/backend_device_test.cpp)
    add_executable(LoBackendCacheTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/backend_cache_test.cpp ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_link_libraries(LoBackendCacheTest PRIVATE fmt::fmt)
    add_executable(LoBackendLifecycleTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/backend_lifecycle_test.cpp
        ${LO_RUNTIME_ROOT}/os/log_file.cpp
        ${LO_RUNTIME_ROOT}/gpu/presentation.cpp ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    # video.cpp reads LO_HAS_FSR / LO_HAS_FSR_D3D12; this fixture links no FSR backend.
    target_compile_definitions(LoBackendLifecycleTest PRIVATE LO_GPU_PLUME SDL_MAIN_HANDLED LO_HAS_FSR=0 LO_HAS_FSR_D3D12=0)
    target_link_options(LoBackendLifecycleTest PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
    target_link_libraries(LoBackendLifecycleTest PRIVATE plume d3d12 dxgi fmt::fmt SDL3::SDL3-static xxHash::xxhash)
    foreach(target LoBackendDeviceTest LoBackendCacheTest LoBackendLifecycleTest)
        target_include_directories(${target} PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
        target_compile_definitions(${target} PRIVATE NOMINMAX)
        target_compile_options(${target} PRIVATE /UNDEBUG /utf-8)
        target_compile_features(${target} PRIVATE cxx_std_20)
        set_target_properties(${target} PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    endforeach()
    target_link_libraries(LoBackendDeviceTest PRIVATE plume d3d12 dxgi)
    add_executable(LoShaderStartupCacheTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/shader_startup_cache_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoShaderStartupCacheTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoShaderStartupCacheTest PRIVATE NOMINMAX)
    target_link_libraries(LoShaderStartupCacheTest PRIVATE fmt::fmt)
    target_compile_options(LoShaderStartupCacheTest PRIVATE /UNDEBUG /utf-8)
    target_compile_features(LoShaderStartupCacheTest PRIVATE cxx_std_20)
    set_target_properties(LoShaderStartupCacheTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    add_executable(LoVulkanBackendTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/vulkan_backend_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/xenos_translator.cpp
        ${LO_RUNTIME_ROOT}/gpu/presentation.cpp)
    target_include_directories(LoVulkanBackendTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoVulkanBackendTest PRIVATE LO_GPU_PLUME NOMINMAX)
    target_compile_features(LoVulkanBackendTest PRIVATE cxx_std_20)
    target_link_libraries(LoVulkanBackendTest PRIVATE plume d3d12 dxgi fmt::fmt)
    add_executable(LoVulkanTextureReuseTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/vulkan_texture_reuse_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoVulkanTextureReuseTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoVulkanTextureReuseTest PRIVATE NOMINMAX)
    target_compile_features(LoVulkanTextureReuseTest PRIVATE cxx_std_20)
    set_target_properties(LoVulkanTextureReuseTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    target_link_libraries(LoVulkanTextureReuseTest PRIVATE plume d3d12 dxgi fmt::fmt)
    add_executable(LoPresentCaptureTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/present_capture_test.cpp
        ${LO_RUNTIME_ROOT}/os/log_file.cpp
        ${LO_RUNTIME_ROOT}/os/capture_archive.cpp
        ${LO_RUNTIME_ROOT}/gpu/presentation.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    foreach(mode PLUME OFF UNIT)
        set(preprocess LoPresentCapturePreprocess${mode})
        add_library(${preprocess} OBJECT ${CMAKE_SOURCE_DIR}/tools/tests/present_capture_preprocess.cpp)
        target_include_directories(${preprocess} PRIVATE ${LO_RUNTIME_ROOT})
        target_compile_features(${preprocess} PRIVATE cxx_std_20)
        target_compile_definitions(${preprocess} PRIVATE LO_PRESENT_CAPTURE_MODE_${mode} NOMINMAX)
        set_target_properties(${preprocess} PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
        target_link_libraries(LoPresentCaptureTest PRIVATE ${preprocess})
    endforeach()
    target_compile_definitions(LoPresentCaptureTest PRIVATE LO_GPU_PLUME SDL_MAIN_HANDLED NOMINMAX LO_HAS_FSR=0 LO_HAS_FSR_D3D12=0)
    target_include_directories(LoPresentCaptureTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_options(LoPresentCaptureTest PRIVATE /UNDEBUG /utf-8)
    target_compile_features(LoPresentCaptureTest PRIVATE cxx_std_20)
    set_target_properties(LoPresentCaptureTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    target_link_options(LoPresentCaptureTest PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
    target_link_libraries(LoPresentCaptureTest PRIVATE plume d3d12 dxgi fmt::fmt SDL3::SDL3-static xxHash::xxhash)
endif()

if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND LO_BUILD_GPU)
    add_executable(LoHdrSurfaceTest EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/tools/tests/hdr_surface_test.cpp)
    target_compile_features(LoHdrSurfaceTest PRIVATE cxx_std_20)
    target_link_libraries(LoHdrSurfaceTest PRIVATE plume SDL3::SDL3-static ${CMAKE_DL_LIBS})
endif()

if(WIN32 AND LO_BUILD_GPU)
    # Deferred EDRAM clears: clearing at once, loadOp CLEAR and deferred clears
    # read back the same bytes (D3D12, or Vulkan with --vulkan; under Proton the
    # same executable reaches vkd3d-proton and RADV).
    add_executable(LoLazyClearGpuTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/lazy_clear_gpu_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoLazyClearGpuTest PRIVATE ${LO_RUNTIME_ROOT} ${CMAKE_SOURCE_DIR}/thirdparty/plume)
    target_compile_features(LoLazyClearGpuTest PRIVATE cxx_std_20)
    target_compile_definitions(LoLazyClearGpuTest PRIVATE NOMINMAX)
    set_target_properties(LoLazyClearGpuTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    target_link_libraries(LoLazyClearGpuTest PRIVATE plume d3d12 dxgi fmt::fmt)
endif()

if(APPLE AND LO_BUILD_GPU)
    add_executable(LoMetalTextureCopyTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/metal_texture_copy_test.mm)
    set_target_properties(LoMetalTextureCopyTest PROPERTIES OBJCXX_STANDARD 20)
    target_compile_options(LoMetalTextureCopyTest PRIVATE -fobjc-arc)
    target_link_libraries(LoMetalTextureCopyTest PRIVATE plume
        "-framework AppKit" "-framework QuartzCore" "-framework Metal" "-framework IOKit")
    add_test(NAME metal_texture_copy COMMAND LoMetalTextureCopyTest)
    set_tests_properties(metal_texture_copy PROPERTIES TIMEOUT 30 LABELS "gpu;macos")

    # The same HDR readback fixture runs on a Mac with a Metal device.
    add_executable(LoPresentationTest EXCLUDE_FROM_ALL
        ${CMAKE_SOURCE_DIR}/tools/tests/presentation_test.cpp
        ${LO_RUNTIME_ROOT}/gpu/presentation.cpp
        ${LO_RUNTIME_ROOT}/gpu/shader/dxc_compiler.cpp)
    target_include_directories(LoPresentationTest PRIVATE $<TARGET_PROPERTY:LostOdysseyRecomp,INCLUDE_DIRECTORIES>)
    target_compile_definitions(LoPresentationTest PRIVATE LO_GPU_PLUME NOMINMAX)
    target_compile_features(LoPresentationTest PRIVATE cxx_std_20)
    set_target_properties(LoPresentationTest PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
    target_link_libraries(LoPresentationTest PRIVATE plume fmt::fmt ${CMAKE_DL_LIBS})
endif()
endif() # NOT ANDROID: desktop test executables

# Package the same SPIR-V-capable DXC beside runtime and compiler/GPU fixtures.
# The Windows SDK compiler is allowed as an explicit fallback, but is not the release contract.
# One custom target stages each file once. Per-target POST_BUILD copies of the
# same destination raced on macOS: CMake's copy unlinks and re-creates the
# destination, so two concurrent copies failed the build with ENOENT.
set(LO_DXC_RUNTIME_FILES)
set(LO_DXC_RUNTIME_TARGETS)
if(WIN32 AND LO_BUILD_GPU)
    set(LO_DXC_BINARY_DIR "${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/bin/x64")
    foreach(binary dxcompiler.dll dxil.dll)
        list(APPEND LO_DXC_RUNTIME_FILES "${LO_DXC_BINARY_DIR}/${binary}")
    endforeach()
    set(LO_DXC_RUNTIME_TARGETS LostOdysseyRecomp LoShaderTool LoShaderAluTest LoPresentationTest LoTemporalAATest LoAmbientOcclusionGpuTest LoRenderResolutionShaderTest LoVulkanBackendTest LoVulkanTextureReuseTest LoShaderStartupCacheTest LoBackendCacheTest LoBackendLifecycleTest LoPresentCaptureTest LoLazyClearGpuTest)
elseif(UNIX AND NOT ANDROID AND LO_BUILD_GPU)
    if(LO_TARGET_PLATFORM STREQUAL "macos")
        # Universal (arm64 + x86_64) build shipped with dxc-bin.
        set(LO_DXC_SO "${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/lib/arm64/libdxcompiler.dylib")
    else()
        string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" LO_DXC_PROCESSOR)
        if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND LO_DXC_PROCESSOR MATCHES "^(aarch64|arm64)$")
            set(LO_DXC_ARCH "arm64")
        elseif(LO_DXC_PROCESSOR MATCHES "^(x86_64|amd64|x64)$")
            set(LO_DXC_ARCH "x64")
        else()
            set(LO_DXC_ARCH "")
        endif()

        if(LO_DXC_ARCH)
            set(LO_DXC_SO "${CMAKE_SOURCE_DIR}/tools/XenosRecomp/thirdparty/dxc-bin/lib/${LO_DXC_ARCH}/libdxcompiler.so")
        else()
            set(LO_DXC_SO "")
            message(STATUS "No bundled Linux DXC architecture mapping for '${CMAKE_SYSTEM_PROCESSOR}'")
        endif()
    endif()
    if(LO_DXC_SO AND EXISTS "${LO_DXC_SO}")
        install(FILES "${LO_DXC_SO}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
        list(APPEND LO_DXC_RUNTIME_FILES "${LO_DXC_SO}")
    endif()
    set(LO_DXC_RUNTIME_TARGETS LostOdysseyRecomp LoShaderTool LoRenderResolutionShaderTest)
endif()
if(LO_DXC_RUNTIME_FILES)
    # Every target in the list links into this directory (no OUTPUT_DIRECTORY override).
    set(LO_DXC_STAGED_FILES)
    foreach(source IN LISTS LO_DXC_RUNTIME_FILES)
        get_filename_component(LO_DXC_FILE_NAME "${source}" NAME)
        set(staged "${CMAKE_CURRENT_BINARY_DIR}/${LO_DXC_FILE_NAME}")
        add_custom_command(OUTPUT "${staged}"
            COMMAND ${CMAKE_COMMAND} -E copy "${source}" "${staged}"
            DEPENDS "${source}"
            COMMENT "Staging ${LO_DXC_FILE_NAME}" VERBATIM)
        list(APPEND LO_DXC_STAGED_FILES "${staged}")
    endforeach()
    add_custom_target(lo_dxc_runtime DEPENDS ${LO_DXC_STAGED_FILES})
    foreach(target IN LISTS LO_DXC_RUNTIME_TARGETS)
        if(TARGET ${target})
            add_dependencies(${target} lo_dxc_runtime)
        endif()
    endforeach()
endif()
