# port/ — game glue + decomp game sources compiled for the host. Kept standalone-capable so the
# decomp compile can iterate without rebuilding libultraship.
cmake_minimum_required(VERSION 3.20)
if(NOT DEFINED PROJECT_NAME)
    project(gdiffuser_port LANGUAGES C CXX)
endif()

find_package(Python3 COMPONENTS Interpreter REQUIRED)

# Warning flags for the port's own targets under GCC/Clang; the decomp library and vendored cxd4
# stay -w (not ours to fix). No -Werror: a Linux first-configure should surface warnings, not fail.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    set(GDX_PORT_WARN_FLAGS -Wall -Wno-unused-parameter)
else()
    set(GDX_PORT_WARN_FLAGS "")
endif()

# Decomp game-source list + option-driven filters, shared with the 3DS build
# (port/3ds/game/CMakeLists.txt). Defines GAME_SOURCES, DECOMP, GDX_GAME_CORE_SOURCES,
# GDX_GAME_EK_PORT_SOURCES, GDX_GAME_DEFS, GDX_DEV_TOOLS_DEFS, GDX_JP_INPUT_DEFS and the
# GDX_EXPANSION_KIT / GDX_FORCE_DEV_TOOLS / GDX_ALLOW_JP_INPUTS options.
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/GdxGameSources.cmake")

add_library(gdiffuser_game OBJECT ${GAME_SOURCES} ${GDX_GAME_CORE_SOURCES})

if(GDX_EXPANSION_KIT)
    target_sources(gdiffuser_game PRIVATE ${GDX_GAME_EK_PORT_SOURCES})
    # MFS_VERSION=1 and BUILD_VERSION=VERSION_J (2.0J libultra API shapes) match the decomp
    # Makefile's EK configuration; sethwinterrupt.c and the headers are gated on it.
    target_compile_definitions(gdiffuser_game PRIVATE EXPANSION_KIT=1 MFS_VERSION=1 BUILD_VERSION=VERSION_J)
endif()

# Fiber backend for the scheduler (n64_sched.c -> gdx_fiber.h): Win32 fibers on Windows,
# POSIX ucontext elsewhere.
if(WIN32)
    target_sources(gdiffuser_game PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gdx_fiber_win32.c")
else()
    target_sources(gdiffuser_game PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gdx_fiber_ucontext.c")
endif()

target_include_directories(gdiffuser_game PRIVATE
    "${DECOMP}"            # decomp root: the C uses includes relative to it (e.g. "src/audio/...")
    "${DECOMP}/include"
    "${DECOMP}/src"
    "${CMAKE_CURRENT_SOURCE_DIR}/../include"   # Torch-generated asset headers (arrays + #defines)
)

target_compile_definitions(gdiffuser_game PRIVATE
    ${GDX_DEV_TOOLS_DEFS}
    ${GDX_GAME_DEFS}       # PORT/GDIFFUSER_PORT/NON_MATCHING/... (cmake/GdxGameSources.cmake)
    VERSION_US=1
    ASSET_VERSION=us       # used by ASSET_HEADER() to build include paths
    ASSET_REVISION=rev0
)

# MSVC: silence IDO-era decomp noise, but keep fatal the classes that are UB on x64 and have
# caused real crashes: C4715/C4716 (non-void falls off the end — MIPS left the value in $v0,
# MSVC returns garbage) and C4013 (implicit decl truncates a 64-bit pointer return to EAX;
# Course Edit's "Test Course" crashed this way for want of one prototype).
# They MUST be spelled /we, not /w1NNNN: /W0 outranks /w1NNNN and would silence them again. /we
# is independent of the warning level.
if(MSVC)
    target_compile_options(gdiffuser_game PRIVATE /W0 /we4715 /we4716 /we4013)
else()
    # GCC/Clang: -w like /W0, but -Werror=return-type keeps the falls-off-the-end class fatal
    # (same x64 crash source as /we4715). GCC 14+ promotes int-conversion /
    # incompatible-pointer-types / implicit-function-declaration to default ERRORS that -w does
    # not downgrade; the IDO-era decomp mixes pointers and 32-bit ints freely, so downgrade those
    # back to silenced warnings. -fcommon: the generated bindings and decomp asset TUs carry
    # tentative definitions of the same symbols (pre-GCC-10/MSVC merged them; GCC 10+ hard-errors).
    # -fno-strict-aliasing / -fwrapv are LOAD-BEARING at -O2/-O3: the decomp type-puns constantly
    # and assumes wrapping arithmetic; TBAA miscompiled the Release audio command path (near-silent
    # audio, ai-tap rms 482 vs Debug's 2498) while -O0 was healthy. Keep on ALL build types.
    target_compile_options(gdiffuser_game PRIVATE -w -Werror=return-type -fcommon
        -fno-strict-aliasing -fwrapv
        -Wno-error=int-conversion
        -Wno-error=incompatible-pointer-types
        -Wno-error=implicit-function-declaration)
endif()

# cxd4 RSP interpreter (LLE audio), vendored under rsp/cxd4 (CC0). Static lib so its C89 sources
# get isolated quiet flags and its module-level globals stay out of the game TUs. Driven by
# gdx_audio_lle.c via gdx_rsp_driver.h. gdx_rsp_boot_tests compiles its own copy, so this lib
# is game-only.
add_library(cxd4_rsp STATIC
    rsp/cxd4/gdx_rsp_driver.c
    rsp/cxd4/su.c
    rsp/cxd4/vu/add.c
    rsp/cxd4/vu/divide.c
    rsp/cxd4/vu/logical.c
    rsp/cxd4/vu/multiply.c
    rsp/cxd4/vu/select.c
    rsp/cxd4/vu/vu.c)
target_include_directories(cxd4_rsp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/rsp/cxd4")
# SP_EXECUTE_LOG enables cxd4's per-instruction hook so the driver's watchdog
# (gdx_rsp_driver.c step_SP_commands) can longjmp out of a runaway/mis-marshalled task
# instead of spinning the audio thread forever.
target_compile_definitions(cxd4_rsp PRIVATE SP_EXECUTE_LOG=1)
if(MSVC)
    target_compile_options(cxd4_rsp PRIVATE /W0)
    target_compile_definitions(cxd4_rsp PRIVATE _CRT_SECURE_NO_WARNINGS=1)
else()
    # Quiet like /W0; -fno-strict-aliasing / -fwrapv for the same miscompile reason as
    # gdiffuser_game above (the RSP core type-puns DMEM/registers and relies on wraparound).
    target_compile_options(cxd4_rsp PRIVATE -w -fno-strict-aliasing -fwrapv)
    target_compile_definitions(cxd4_rsp PRIVATE _CRT_SECURE_NO_WARNINGS=1)
endif()

# Discord Rich Presence: vendored static discord-rpc (see its CMakeLists for provenance and the
# strip rationale). EXCLUDE_FROM_ALL keeps it out of the test targets' builds.
add_subdirectory(third_party/discord-rpc EXCLUDE_FROM_ALL)

add_executable(G-Diffuser
    main.cpp
    gdx_discord.cpp
    gdx_gui.cpp
    gdx_menu.cpp
    gdx_menu_registry.cpp
    gdx_console_log.cpp
    ui/UIWidgets.cpp           # ported from HarbourMasters/Lighthouse, CC0 1.0
    enhancements/events/GameEvents.cpp      # REQUIRED even with every enhancement off: decomp
                                # racer.c references GameEvents_FireOnBoostStart, so dropping this
                                # is a link failure, not a missing feature.
    enhancements/tuning/BoostDuration.cpp   # self-registers from a static initializer; nothing
                                # references it, so it looks like dead code and is not.
    input_bridge.c
    gdx_hud_placement.c        # nihudtop Phase 3: split out of input_bridge.c so the predicate
                                # is host-testable standalone (port/tests/)
    gdx_input_script.c
    gdx_input_rec.c            # ORACLE: .gdxrec record/replay stage of the unified injector
    gdx_determinism.c          # ORACLE: Math_Rand seed / boot-hold pins the decomp reads
    n64_gfx_bridge.cpp
    gdx_interp.cpp
    n64_gfx_convert.cpp
    gdx_vi_convert.c
    n64_audio_hle.c            # standalone C, deliberately not part of gdiffuser_game
    n64_audio_hle_fast.c       # [debug] audio_simd: the bit-exact fast kernels (C4k)
    gdx_audio_hle_cost.c       # [synx]: HLE synth CPU-vs-wall ledger (C4k)
    gdx_ndsp_hash.c            # [debug] ndsphash; its taps live inside n64_audio_hle.c
    gdx_audio_mix_census.c     # [debug] audiomix; its taps live inside n64_audio_hle.c too
    gdx_audio_lle.c
    gdx_audio_thread.cpp
    gdx_audio_capture.c
    gdx_frame_pacer.c          # default OFF: LUS's Fast3D backend already paces EndFrame() to 60fps
    shims.c
    AssetLoader.cpp
    resource/ResourceFactories.cpp
    gdx_firstboot.cpp
    gdx_extract_launch.cpp
    gdx_dump_launch.cpp
    gdx_firstboot_gui.cpp
    rom_buffer.cpp
    sram_buffer.cpp
    gdx_save_replace.c
    disk_savefile.cpp
    gdx_ghost_io.c
    gdx_ghost_window.cpp
    gdx_input_viewer.cpp
    gdx_fps_overlay.cpp
    gdx_perf.cpp
    gdx_dev_gates.c            # dependency-free C: CVar entry points arrive as function pointers
                                # from main.cpp, so the standalone test targets compile it unchanged
    gdx_imgui_nav.cpp
    gdx_workshop.cpp
    mio0_wrap.c
    gdx_stall.c                # A10 load-stall census (no-op unless armed)
    gdx_segment_source.c       # in the exe, not gdiffuser_game: needs symbols from AssetLoader.cpp,
                                # rom_buffer.cpp and gen/AssetBindings.c
    gdx_linkstubs_desktop.c    # symbols the DEFAULT config gets from a TU a non-default config
                                # does not compile. Sibling of gdx3ds_us_linkstubs.c; behaviour
                                # is unchanged in the shipped configuration. See the file.
    "${CMAKE_CURRENT_SOURCE_DIR}/../torch/lib/libmio0/mio0.c"
    $<TARGET_OBJECTS:gdiffuser_game>)

if(GDX_EXPANSION_KIT)
    # All four files reference EK-only symbols (gdx_ek_assets_fill, machine_create labels, EK
    # overlay .data), so they must not build in US-only configs.
    # gdx_block_cache.cpp is the cache gdx_disk_stream.cpp used to hold in its own file
    # statics; C4d lifted it out so the 3DS audio_table stream could share it rather than be a
    # second LRU. The desktop exe needs it wherever gdx_disk_stream.cpp is built.
    target_sources(G-Diffuser PRIVATE disk_buffer.cpp gdx_disk_stream.cpp gdx_block_cache.cpp
                                      gdx_disk_dcrc.cpp   # B11m: the .dcrc identity-cache sidecar
                                      gen/EkTranslatedOverrides.c
                                      gdx_ek_strings.c gen/EkTranslatedStrings.c
                                      # B11m: the English text pack. EkTranslatedTextPack.c is
                                      # generated and .gitignore'd; it is EMPTY unless
                                      # tools/gen_ek_text_pack.py was run with a translated image.
                                      gdx_ek_text_pack.c gen/EkTranslatedTextPack.c)
    # shims.c guards its US-only Leo stubs on this define.
    target_compile_definitions(G-Diffuser PRIVATE EXPANSION_KIT=1)
endif()

target_compile_definitions(G-Diffuser PRIVATE ${GDX_DEV_TOOLS_DEFS} ${GDX_JP_INPUT_DEFS})

if(WIN32)
    set_target_properties(G-Diffuser PROPERTIES WIN32_EXECUTABLE TRUE)
    # Application icon; Linux desktop entries carry theirs separately.
    target_sources(G-Diffuser PRIVATE gdiffuser.rc)
    # Emit a linker .map so crash RVAs and low32 data addresses in gdiffuser-run.log resolve to
    # symbols without a debugger.
    #
    # DO NOT add /DYNAMICBASE:NO. It was tried, to close the ASLR low32-collision class (a random
    # image base whose low32 lands in an N64 address window misroutes reconstructed 32-bit tokens
    # — see the KSEG guard in decomp/src/sys/dma.c), and the fixed base 0x140000000 was worse:
    # module low32s at 0x40000000..0x41FFFFFF corrupted the translated menu/text textures
    # build-wide and null-write-crashed Fast::Interpreter::ImportTextureRgba32 at startup
    # (GDX_WIDE_ASSET_RESOLVE=0 ruled out the resolver, leaving the base). Until every low32
    # window is audited against a chosen base, a fixed base is a bigger dice roll than ASLR.
    if(MSVC)
        target_link_options(G-Diffuser PRIVATE "/MAP")
    endif()
endif()

# Scrubs a game-derived icon that older build trees still carry; harmless on a clean tree.
add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E rm -f "$<TARGET_FILE_DIR:G-Diffuser>/gdiffuser.png"
    VERBATIM)

file(GLOB_RECURSE GDIFFUSER_F3D_SHADER_SOURCES CONFIGURE_DEPENDS
    "${CMAKE_SOURCE_DIR}/libultraship/src/fast/shaders/*")
# gdiffuser.o2r carries only the MIT-licensed Fast3D shaders.
set(GDIFFUSER_ENGINE_O2R "${CMAKE_CURRENT_BINARY_DIR}/gdiffuser.o2r")
add_custom_command(
    OUTPUT "${GDIFFUSER_ENGINE_O2R}"
    COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/gen_f3d_o2r.py" "${GDIFFUSER_ENGINE_O2R}"
    DEPENDS "${CMAKE_SOURCE_DIR}/tools/gen_f3d_o2r.py"
            "${CMAKE_SOURCE_DIR}/assets/branding/gdiffuser-logo-menu.png"
            ${GDIFFUSER_F3D_SHADER_SOURCES}
    COMMENT "Generating engine archive gdiffuser.o2r (Fast3D shaders)"
    VERBATIM)
add_custom_target(gdiffuser_engine_o2r DEPENDS "${GDIFFUSER_ENGINE_O2R}")
add_dependencies(G-Diffuser gdiffuser_engine_o2r)
add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${GDIFFUSER_ENGINE_O2R}" "$<TARGET_FILE_DIR:G-Diffuser>/gdiffuser.o2r"
    VERBATIM)

# Fonts stay loose beside the exe so GdxFast3dGui can add them to ImGui's atlas before the
# renderer uploads it.
set(GDIFFUSER_GUI_FONT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts")
add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E make_directory "$<TARGET_FILE_DIR:G-Diffuser>/fonts"
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${GDIFFUSER_GUI_FONT_DIR}/Montserrat-Regular.ttf"
            "${GDIFFUSER_GUI_FONT_DIR}/Inconsolata-Regular.ttf"
            "${GDIFFUSER_GUI_FONT_DIR}/Montserrat-OFL.txt"
            "${GDIFFUSER_GUI_FONT_DIR}/Inconsolata-OFL.txt"
            "$<TARGET_FILE_DIR:G-Diffuser>/fonts"
    VERBATIM)

# Keep the project license and every bundled/static dependency notice with binary releases.
set(GDIFFUSER_LICENSE_DIR "${CMAKE_SOURCE_DIR}/LICENSES")
add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${CMAKE_SOURCE_DIR}/LICENSE"
            "${CMAKE_SOURCE_DIR}/THIRD_PARTY_NOTICES.md"
            "$<TARGET_FILE_DIR:G-Diffuser>"
    COMMAND "${CMAKE_COMMAND}" -E copy_directory
            "${GDIFFUSER_LICENSE_DIR}"
            "$<TARGET_FILE_DIR:G-Diffuser>/LICENSES"
    VERBATIM)

# gdx-extract, the standalone deterministic O2R extractor first boot spawns as a child process.
# Isolated ExternalProject, NOT add_subdirectory'd:
# Torch pulls its own spdlog/fmt/yaml-cpp via FetchContent, and out-of-tree prevents ODR/version
# clashes with libultraship's spdlog/fmt (docs/investigation/.../W0_TORCH_DEPENDENCIES.md).
# GDX_DETERMINISTIC=ON => byte-identical archives for identical ROM + recipes. CMP0091=NEW lets
# Torch's /MT runtime take effect so the exe needs no VC++ redist. gdx-extract-fp.cmake turns off
# FMA contraction, without which an arm64 build misses the golden (see that file).
include(ExternalProject)

set(GDX_EXTRACT_SRC_DIR "${CMAKE_SOURCE_DIR}/torch")
set(GDX_EXTRACT_PREFIX  "${CMAKE_BINARY_DIR}/gdx-extract")
set(GDX_EXTRACT_INSTALL "${GDX_EXTRACT_PREFIX}/install")
set(GDX_EXTRACT_EXE     "${GDX_EXTRACT_INSTALL}/bin/gdx-extract${CMAKE_EXECUTABLE_SUFFIX}")

ExternalProject_Add(gdx_extract_ext
    SOURCE_DIR  "${GDX_EXTRACT_SRC_DIR}"
    PREFIX      "${GDX_EXTRACT_PREFIX}"
    INSTALL_DIR "${GDX_EXTRACT_INSTALL}"
    CMAKE_CACHE_ARGS
        -DUSE_STANDALONE:BOOL=ON
        -DBUILD_STORMLIB:BOOL=OFF
        -DGDX_DETERMINISTIC:BOOL=ON
        -DCMAKE_PROJECT_torch_INCLUDE:FILEPATH=${CMAKE_SOURCE_DIR}/tools/prebake/gdx-extract-fp.cmake
        -DGDX_OUTPUT_NAME:STRING=gdx-extract
        -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
        -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
        -DCMAKE_POLICY_DEFAULT_CMP0091:STRING=NEW
    BUILD_COMMAND   "${CMAKE_COMMAND}" --build   <BINARY_DIR> --config $<CONFIG>
    INSTALL_COMMAND "${CMAKE_COMMAND}" --install <BINARY_DIR> --config $<CONFIG>
    # BUILD_ALWAYS ON: ExternalProject stamps do not track torch/ source changes, so OFF served a
    # stale extractor from the install prefix (bit us twice). Incremental no-change rebuild is
    # a few seconds; correctness wins.
    BUILD_ALWAYS ON
    BUILD_BYPRODUCTS "${GDX_EXTRACT_EXE}")

add_custom_target(gdx-extract DEPENDS gdx_extract_ext)
add_dependencies(G-Diffuser gdx_extract_ext)

add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${GDX_EXTRACT_EXE}"
            "$<TARGET_FILE_DIR:G-Diffuser>/gdx-extract${CMAKE_EXECUTABLE_SUFFIX}"
    VERBATIM)

# Deploy the recipe tree as decomp-recipes/ next to the exe. The packaged layout mirrors the
# decomp source tree verbatim so config.yml needs no edits (Torch resolves paths relative to it).
# Only us/rev0 ships; other revisions' config keys are never dereferenced because recipe
# selection is by matched ROM SHA1.
set(GDX_RECIPES_SRC "${CMAKE_SOURCE_DIR}/decomp")
set(GDX_RECIPES_DST "$<TARGET_FILE_DIR:G-Diffuser>/decomp-recipes")
# The EK slice manifest ships beside the recipes so the runtime disk step can pass it to
# `gdx-extract disk -m`.
set(GDX_EK_SLICE_MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/gen/ek_slice_manifest.txt")
add_custom_command(TARGET G-Diffuser POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E make_directory "${GDX_RECIPES_DST}/assets/yaml/us"
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${GDX_RECIPES_SRC}/config.yml"
            "${GDX_RECIPES_DST}/config.yml"
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${GDX_RECIPES_SRC}/LICENSE"
            "${GDX_RECIPES_DST}/LICENSE"
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different
            "${GDX_EK_SLICE_MANIFEST}"
            "${GDX_RECIPES_DST}/ek_slice_manifest.txt"
    COMMAND "${CMAKE_COMMAND}" -E copy_directory
            "${GDX_RECIPES_SRC}/assets/yaml/us/rev0"
            "${GDX_RECIPES_DST}/assets/yaml/us/rev0"
    # Dump All data files, generated from the single source of truth at build time: audio tables
    # from decomp audio_tables.c and the EK TLUT map from the EK recipe tree, so the dump classes
    # work on machines without decomp source.
    COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/gen_dump_tables_data.py"
            --audio-tables "${GDX_RECIPES_SRC}/src/audio/disk/audio_tables.c"
            --out-dir "${GDX_RECIPES_DST}"
            --ek-yaml-dir "${CMAKE_SOURCE_DIR}/fzerox-expansion-kit/assets/yaml/jp"
    VERBATIM)

target_include_directories(G-Diffuser PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}"   # for "resource/ResourceFactories.h"
    "${DECOMP}/include"
    "${DECOMP}/src"
    "${CMAKE_CURRENT_SOURCE_DIR}/../torch/lib/libmio0"  # mio0.h + utils.h for mio0_wrap.c and mio0.c
)

target_link_libraries(G-Diffuser PRIVATE cxd4_rsp)
target_link_libraries(G-Diffuser PRIVATE discord-rpc)

if(WIN32)
    # user32: MessageBox; Comdlg32: ROM picker; Comctl32: extraction progress marquee.
    target_link_libraries(G-Diffuser PRIVATE user32 Comdlg32 Comctl32)
else()
    # pthread for the fiber backend + audio thread; dl for dladdr in n64_gfx_bridge.cpp
    # (needed on older glibc, harmless on newer).
    find_package(Threads REQUIRED)
    target_link_libraries(G-Diffuser PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
    # Release tarballs carry the dependencies no distribution can be relied on to provide in
    # lib/ beside the executable (tools/package_linux.sh). $ORIGIN resolves them at load time,
    # which beats a wrapper script because the binary stays directly runnable. No effect on a
    # local build, where lib/ does not exist and the loader falls through to the system paths.
    set_target_properties(G-Diffuser PROPERTIES
        BUILD_WITH_INSTALL_RPATH TRUE
        INSTALL_RPATH "$ORIGIN/lib")
endif()

if(GDX_PORT_WARN_FLAGS)
    target_compile_options(G-Diffuser PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()
# Same UB-hardening as gdiffuser_game/cxd4_rsp: the port glue reinterprets N64 RDRAM through
# host pointers and resolves host ranges by bit-pattern — all strict-aliasing-sensitive.
if(NOT MSVC)
    target_compile_options(G-Diffuser PRIVATE -fno-strict-aliasing -fwrapv)
    # Linux audio fix: the decomp's sinf/cosf were exported from .dynsym and interposed the ones
    # libSDL3's resampler imports -> silent/garbled audio. The version script localizes them so
    # libSDL3 gets glibc's. Full writeup in port/gdx_hide_libm.map.
    target_link_options(G-Diffuser PRIVATE
        "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/gdx_hide_libm.map")
endif()
if(TARGET libultraship)
    target_link_libraries(G-Diffuser PRIVATE libultraship)
    target_include_directories(G-Diffuser PRIVATE
        "${CMAKE_SOURCE_DIR}/libultraship/src"
        "${CMAKE_SOURCE_DIR}/libultraship/include"
    )
endif()

# DSP unit-test harness: compiles n64_audio_hle.c UNMODIFIED and drives it black-box through
# gdx_audio_hle_run (design rationale in dsp_tests.c's header). Stand-ins for the two
# gdx_resolve_*_host_address externs live in dsp_tests.c, so n64_gfx_bridge.cpp stays out.
# gdx_dev_gates.c is linked because n64_audio_hle.c and port_log.h read the gate cache; no
# console is bound, so gates keep their env-seeded values as the _putenv-based tests expect.
add_executable(gdx_dsp_tests
    tests/dsp_tests.c
    n64_audio_hle.c
    n64_audio_hle_fast.c # C4k fast kernels (lever default off; the parity test flips it)
    gdx_audio_hle_cost.c # C4k [synx] ledger (never armed here)
    gdx_ndsp_hash.c   # n64_audio_hle.c's [ndsphash] taps; inert here (never configured on)
    gdx_audio_mix_census.c # [audiomix]: armed BY these tests, see TEST 21
    gdx_dev_gates.c)
if(MSVC)
    target_compile_options(gdx_dsp_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_dsp_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
    target_link_libraries(gdx_dsp_tests PRIVATE m) # sin/floor: libm is separate on UNIX
endif()

# [ndsphash] unit test: compiles gdx_ndsp_hash.c UNMODIFIED and drives its public API through
# the test-only configure/reset seams. No game objects, no decomp headers, no DSP -- the point
# is the MAP (content-addressed key -> rendered value) and its two failure modes, which are
# assertable without an emulator. Mirrored into tools/ci-3ds.sh as gdx3ds_ndsphash_host_tests.
add_executable(gdx3ds_ndsphash_tests
    tests/ndsphash_tests.c
    gdx_ndsp_hash.c)
if(MSVC)
    target_compile_options(gdx3ds_ndsphash_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx3ds_ndsphash_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# PCM capture unit test: compiles gdx_audio_capture.c UNMODIFIED and drives its public API plus
# the test-only reset seam. No game objects, no libultraship, no decomp headers.
add_executable(gdx_pcm_capture_tests
    tests/pcm_capture_tests.c
    gdx_audio_capture.c
    gdx_dev_gates.c)   # port_log.h's log/trace gates read the developer-gate cache
if(MSVC)
    target_compile_options(gdx_pcm_capture_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_pcm_capture_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# VI-scanout fallback unit test: compiles gdx_vi_convert.c UNMODIFIED and drives
# gdx_convert_rgba5551_to_rgba8888 with known pixel patterns. No game objects, no libultraship.
add_executable(gdx_vi_fallback_tests
    tests/test_vi_fallback.c
    gdx_vi_convert.c)
if(MSVC)
    target_compile_options(gdx_vi_fallback_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_vi_fallback_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# Wide-Gfx packing test: compiles only the real decomp gbi.h with the game's defines, so the
# gSP*/gDP* macros under test are exactly the ones the game emits. Verifies w1 holds a full
# 64-bit host pointer and a bridge-style reader recovers it. See tests/gfx_pack_tests.c.
add_executable(gdx_gfx_pack_tests tests/gfx_pack_tests.c)
target_include_directories(gdx_gfx_pack_tests PRIVATE "${DECOMP}/include")
target_compile_definitions(gdx_gfx_pack_tests PRIVATE
    PORT=1
    GDIFFUSER_PORT=1
    F3DEX_GBI_2=1
    _LANGUAGE_C=1
    VERSION_US=1)
if(MSVC)
    target_compile_options(gdx_gfx_pack_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_gfx_pack_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# Binary-DL converter test: compiles n64_gfx_convert.cpp UNMODIFIED and drives its public API
# with synthetic 8-byte N64 display lists. No game objects, no libultraship.
add_executable(gdx_gfx_convert_tests
    tests/gfx_convert_tests.cpp
    n64_gfx_convert.cpp)
target_include_directories(gdx_gfx_convert_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
if(MSVC)
    target_compile_options(gdx_gfx_convert_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_gfx_convert_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# RGBA16 TMEM-decode extent test: reproduces the tall-HUD-atlas height overshoot that garbled
# the 3DS speedometer/km-h/symbol strips. Standalone, no libultraship. See the file header and
# port/3ds/patches/lus-3ds-hud-tall-atlas-extent.patch.
add_executable(gdx_gfx_rgba16_extent_tests tests/gfx_rgba16_extent_tests.cpp)
target_include_directories(gdx_gfx_rgba16_extent_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
if(MSVC)
    target_compile_options(gdx_gfx_rgba16_extent_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_gfx_rgba16_extent_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# Machine drop-shadow sampling-contract test: pins the facts that make the pow2-padded
# mirror seam irrelevant for the shadow draw (decode extent 32x64, identity padding,
# interior quad UVs, N64 clamp-before-mask, clamp-strip routing). Standalone, no
# libultraship. See the file header and docs/research/shadow2-mirror-period-audit.md.
add_executable(gdx_gfx_shadow_uv_tests tests/gfx_shadow_uv_tests.cpp)
target_include_directories(gdx_gfx_shadow_uv_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
if(MSVC)
    target_compile_options(gdx_gfx_shadow_uv_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_gfx_shadow_uv_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# OTR-filepath backstop test: reproduces the 32-bit (3DS) drop of every legitimate
# o2r-filepath SETTIMG emit -- the in-race HUD garble root cause. Standalone, no libultraship.
# See the file header and the backstop comment in n64_gfx_bridge.cpp ProcessList.
add_executable(gdx_gfx_filepath_backstop_tests tests/gfx_filepath_backstop_tests.cpp)
target_include_directories(gdx_gfx_filepath_backstop_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
if(MSVC)
    target_compile_options(gdx_gfx_filepath_backstop_tests PRIVATE /W3)
elseif(GDX_PORT_WARN_FLAGS)
    target_compile_options(gdx_gfx_filepath_backstop_tests PRIVATE ${GDX_PORT_WARN_FLAGS})
endif()

# Resource smoke test: links libultraship only (no game objects), so the .o2r pipeline can be
# verified independently of the full game.
if(TARGET libultraship)
    add_executable(resource_smoketest resource_smoketest.cpp resource/ResourceFactories.cpp)
    target_include_directories(resource_smoketest PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}"
        "${CMAKE_SOURCE_DIR}/libultraship/src"
        "${CMAKE_SOURCE_DIR}/libultraship/include"
    )
    target_link_libraries(resource_smoketest PRIVATE libultraship)
endif()

# LLE audio boot test: proves the cxd4 interpreter boots and executes the real aspMain microcode
# with the correct IMEM byte-swap, DMEM endianness, and OSTask boot contract. Skips gracefully if
# the ROM-extracted ucode blobs are absent. See docs/LLE_IMPL_STATE.md.
add_executable(gdx_rsp_boot_tests
    rsp/tests/rsp_boot_test.c
    rsp/cxd4/gdx_rsp_driver.c
    rsp/cxd4/su.c
    rsp/cxd4/vu/add.c
    rsp/cxd4/vu/divide.c
    rsp/cxd4/vu/logical.c
    rsp/cxd4/vu/multiply.c
    rsp/cxd4/vu/select.c
    rsp/cxd4/vu/vu.c)
target_include_directories(gdx_rsp_boot_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/rsp/cxd4")
target_compile_definitions(gdx_rsp_boot_tests PRIVATE
    SP_EXECUTE_LOG=1
    GDX_RSP_TEST_STEP_HOOK=1   # test provides its own step_SP_commands; exclude the driver's watchdog one
    _CRT_SECURE_NO_WARNINGS=1
    "RSP_BLOB_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/rsp\"")
if(MSVC)
    target_compile_options(gdx_rsp_boot_tests PRIVATE /W3)
else()
    # Vendored C89 cxd4 sources dominate this target.
    target_compile_options(gdx_rsp_boot_tests PRIVATE -w)
endif()

# EXPERIMENTAL second binary — G-Diffuser-JP (VERSION_JP profile).
# The port is US-only in three compile-time layers (binding tables, golden constants, VERSION_JP
# decomp branches), so US+JP cannot share one executable; the frozen decision
# (QUALITY_PARITY_ROADMAP §R5) is two binaries from one tree sharing one data dir. Default OFF so
# the default build is byte-unchanged. This does not compile yet: the Torch-generated JP asset
# headers do not exist, the VERSION_JP decomp sites have never been built here, the EK gen files
# are US-EK placeholders, and gen/LinkStubs.jp.c needs regenerating from a JP link log.
option(GDX_BUILD_JP "Build the EXPERIMENTAL second binary G-Diffuser-JP (VERSION_JP profile)" OFF)

if(GDX_BUILD_JP)
    message(STATUS "G-Diffuser: GDX_BUILD_JP=ON — configuring EXPERIMENTAL VERSION_JP target G-Diffuser-JP.")

    # Mirror of gdiffuser_game with the JP profile: only the defines and the generated
    # binding/stub files differ.
    add_library(gdiffuser_game_jp OBJECT ${GAME_SOURCES}
        "${CMAKE_CURRENT_SOURCE_DIR}/decomp_port.c"
        "${CMAKE_CURRENT_SOURCE_DIR}/n64_sched.c"
        "${CMAKE_CURRENT_SOURCE_DIR}/n64_vi.c"
        "${DECOMP}/src/libultra/io/devmgr.c"
        "${CMAKE_CURRENT_SOURCE_DIR}/gen/AssetBindings.jp.c"
        "${CMAKE_CURRENT_SOURCE_DIR}/gen/LinkStubs.jp.c")

    if(GDX_EXPANSION_KIT)
        target_sources(gdiffuser_game_jp PRIVATE
            "${CMAKE_CURRENT_SOURCE_DIR}/n64_leo.c"
            "${CMAKE_CURRENT_SOURCE_DIR}/gen/EkAssetBindings.c"   # US-EK bindings reused; jp/ek TODO
            "${CMAKE_CURRENT_SOURCE_DIR}/gen/EkLinkStubs.c"
            "${CMAKE_CURRENT_SOURCE_DIR}/gdx_ek_disk_overrides.c")
        target_compile_definitions(gdiffuser_game_jp PRIVATE EXPANSION_KIT=1 MFS_VERSION=1 BUILD_VERSION=VERSION_J)
    endif()

    if(WIN32)
        target_sources(gdiffuser_game_jp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gdx_fiber_win32.c")
    else()
        target_sources(gdiffuser_game_jp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/gdx_fiber_ucontext.c")
    endif()

    target_include_directories(gdiffuser_game_jp PRIVATE
        "${DECOMP}"
        "${DECOMP}/include"
        "${DECOMP}/src"
        "${CMAKE_CURRENT_SOURCE_DIR}/../include")   # needs include/assets/jp/rev0 (owner-run)

    # Same define set as gdiffuser_game, but the JP profile triplet.
    target_compile_definitions(gdiffuser_game_jp PRIVATE
        ${GDX_DEV_TOOLS_DEFS}
        PORT=1
        GDIFFUSER_PORT=1
        NON_MATCHING=1
        NON_EQUIVALENT=1
        AVOID_UB=1
        F3DEX_GBI_2=1
        _LANGUAGE_C=1
        VERSION_JP=1
        ASSET_VERSION=jp
        ASSET_REVISION=rev0
    )
    if(MSVC)
        target_compile_options(gdiffuser_game_jp PRIVATE /W0 /w14715 /w14716)
    else()
        target_compile_options(gdiffuser_game_jp PRIVATE -w -Werror=return-type -fcommon
            -fno-strict-aliasing -fwrapv
            -Wno-error=int-conversion
            -Wno-error=incompatible-pointer-types
            -Wno-error=implicit-function-declaration)
    endif()

    # Mirror of G-Diffuser, linking the JP game objects. The registry/widgets/events TUs are
    # REQUIRED here too: this target compiles the same gdx_menu.cpp and PORT-gated racer.c, so
    # omitting them is an unresolved external, not a missing feature.
    add_executable(G-Diffuser-JP
        main.cpp
        gdx_gui.cpp
        gdx_menu.cpp
        gdx_menu_registry.cpp
        gdx_console_log.cpp
        ui/UIWidgets.cpp
        enhancements/events/GameEvents.cpp
        enhancements/tuning/BoostDuration.cpp
        gdx_discord.cpp
        input_bridge.c
        gdx_hud_placement.c
        gdx_input_script.c
        gdx_input_rec.c
        gdx_determinism.c
        n64_gfx_bridge.cpp
        gdx_interp.cpp
        n64_gfx_convert.cpp
        gdx_vi_convert.c
        n64_audio_hle.c
        n64_audio_hle_fast.c
        gdx_audio_hle_cost.c
        gdx_ndsp_hash.c
        gdx_audio_mix_census.c
        gdx_audio_lle.c
        gdx_audio_thread.cpp
        gdx_audio_capture.c
        gdx_frame_pacer.c
        shims.c
        AssetLoader.cpp
        resource/ResourceFactories.cpp
        gdx_firstboot.cpp
        gdx_extract_launch.cpp
        gdx_dump_launch.cpp
        gdx_firstboot_gui.cpp
        rom_buffer.cpp
        sram_buffer.cpp
        gdx_save_replace.c
        disk_savefile.cpp
        gdx_ghost_io.c
        gdx_ghost_window.cpp
        gdx_input_viewer.cpp
        gdx_fps_overlay.cpp
        gdx_perf.cpp
        gdx_dev_gates.c
        gdx_imgui_nav.cpp
        gdx_workshop.cpp
        mio0_wrap.c
        gdx_stall.c
        gdx_segment_source.c
        "${CMAKE_CURRENT_SOURCE_DIR}/../torch/lib/libmio0/mio0.c"
        $<TARGET_OBJECTS:gdiffuser_game_jp>)

    # The exe TUs must see VERSION_JP too: they include decomp headers whose region branches
    # depend on it. GDX_ALLOW_JP_INPUTS is unconditional here regardless of the option — this
    # target IS the Japanese profile; refusing JP data would leave it nothing to load.
    target_compile_definitions(G-Diffuser-JP PRIVATE
        ${GDX_DEV_TOOLS_DEFS}
        PORT=1 GDIFFUSER_PORT=1 F3DEX_GBI_2=1 _LANGUAGE_C=1 VERSION_JP=1
        GDX_ALLOW_JP_INPUTS=1
        ASSET_VERSION=jp ASSET_REVISION=rev0)

    if(GDX_EXPANSION_KIT)
        # EK-only symbols — see the matching US-target comment above.
        target_sources(G-Diffuser-JP PRIVATE disk_buffer.cpp gdx_disk_dcrc.cpp
                                             gen/EkTranslatedOverrides.c
                                             gdx_ek_strings.c gen/EkTranslatedStrings.c
                                             gdx_ek_text_pack.c gen/EkTranslatedTextPack.c)
        target_compile_definitions(G-Diffuser-JP PRIVATE EXPANSION_KIT=1)
    endif()

    if(WIN32)
        set_target_properties(G-Diffuser-JP PROPERTIES WIN32_EXECUTABLE TRUE)
        target_sources(G-Diffuser-JP PRIVATE gdiffuser.rc)
        if(MSVC)
            # No /DYNAMICBASE:NO — see the note on the US target above.
            target_link_options(G-Diffuser-JP PRIVATE "/MAP")
        endif()
    endif()

    target_include_directories(G-Diffuser-JP PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}"
        "${DECOMP}/include"
        "${DECOMP}/src"
        "${CMAKE_CURRENT_SOURCE_DIR}/../include"
        "${CMAKE_CURRENT_SOURCE_DIR}/../torch/lib/libmio0")

    target_link_libraries(G-Diffuser-JP PRIVATE cxd4_rsp)
    target_link_libraries(G-Diffuser-JP PRIVATE discord-rpc)

    if(WIN32)
        target_link_libraries(G-Diffuser-JP PRIVATE user32 Comdlg32 Comctl32)
    else()
        find_package(Threads REQUIRED)
        target_link_libraries(G-Diffuser-JP PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
    endif()

    if(GDX_PORT_WARN_FLAGS)
        target_compile_options(G-Diffuser-JP PRIVATE ${GDX_PORT_WARN_FLAGS})
    endif()
    if(NOT MSVC)
        target_compile_options(G-Diffuser-JP PRIVATE -fno-strict-aliasing -fwrapv)
        target_link_options(G-Diffuser-JP PRIVATE
            "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/gdx_hide_libm.map")
    endif()
    if(TARGET libultraship)
        target_link_libraries(G-Diffuser-JP PRIVATE libultraship)
        target_include_directories(G-Diffuser-JP PRIVATE
            "${CMAKE_SOURCE_DIR}/libultraship/src"
            "${CMAKE_SOURCE_DIR}/libultraship/include")
    endif()

    # Deploy engine archive + extractor + recipe tree (US rev0 AND JP rev0 so one binary can
    # build either profile's archive).
    add_dependencies(G-Diffuser-JP gdiffuser_engine_o2r gdx_extract_ext)
    add_custom_command(TARGET G-Diffuser-JP POST_BUILD
        COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${GDIFFUSER_ENGINE_O2R}" "$<TARGET_FILE_DIR:G-Diffuser-JP>/gdiffuser.o2r"
        COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${GDX_EXTRACT_EXE}" "$<TARGET_FILE_DIR:G-Diffuser-JP>/gdx-extract${CMAKE_EXECUTABLE_SUFFIX}"
        VERBATIM)
    add_custom_command(TARGET G-Diffuser-JP POST_BUILD
        COMMAND "${CMAKE_COMMAND}" -E make_directory "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/assets/yaml/us"
        COMMAND "${CMAKE_COMMAND}" -E make_directory "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/assets/yaml/jp"
        COMMAND "${CMAKE_COMMAND}" -E copy_if_different
                "${GDX_RECIPES_SRC}/config.yml" "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/config.yml"
        COMMAND "${CMAKE_COMMAND}" -E copy_if_different
                "${GDX_EK_SLICE_MANIFEST}" "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/ek_slice_manifest.txt"
        COMMAND "${CMAKE_COMMAND}" -E copy_directory
                "${GDX_RECIPES_SRC}/assets/yaml/us/rev0" "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/assets/yaml/us/rev0"
        COMMAND "${CMAKE_COMMAND}" -E copy_directory
                "${GDX_RECIPES_SRC}/assets/yaml/jp/rev0" "$<TARGET_FILE_DIR:G-Diffuser-JP>/decomp-recipes/assets/yaml/jp/rev0"
        VERBATIM)
endif()
